From 74e0f331f5197db8e10230583c1605c28a9be720 Mon Sep 17 00:00:00 2001 From: Mateusz Gozdek Date: Wed, 3 Jun 2020 10:28:29 +0200 Subject: [PATCH 001/237] Add missing import of QPainterPath (#254, Fix #253) --- gui/src/servericonwidget.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/gui/src/servericonwidget.cpp b/gui/src/servericonwidget.cpp index e9d4e08..f7d60d7 100644 --- a/gui/src/servericonwidget.cpp +++ b/gui/src/servericonwidget.cpp @@ -18,6 +18,7 @@ #include #include +#include ServerIconWidget::ServerIconWidget(QWidget *parent) : QWidget(parent) { From 2f8d9d189d1e66fe8b91e8f81567d57c76a63b43 Mon Sep 17 00:00:00 2001 From: Roy P Date: Mon, 15 Jun 2020 12:56:09 -0400 Subject: [PATCH 002/237] Pin the flatpak file to the v1.2.1 release --- scripts/flatpak/com.github.thestr4ng3r.Chiaki.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/scripts/flatpak/com.github.thestr4ng3r.Chiaki.json b/scripts/flatpak/com.github.thestr4ng3r.Chiaki.json index 831c079..abd3710 100644 --- a/scripts/flatpak/com.github.thestr4ng3r.Chiaki.json +++ b/scripts/flatpak/com.github.thestr4ng3r.Chiaki.json @@ -88,7 +88,9 @@ "sources": [ { "type": "git", - "url": "https://github.com/thestr4ng3r/chiaki.git" + "url": "https://github.com/thestr4ng3r/chiaki.git", + "tag": "v1.2.1", + "commit": "89c9bd44c354d1351fb9b9a8f9f0aade8077a129" } ] } From ec7b46ac67a7c535c14d210339fe9bf969af4d80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Sun, 21 Jun 2020 14:19:30 +0200 Subject: [PATCH 003/237] Begin libsetsu with udev scan --- .gitignore | 2 + CMakeLists.txt | 1 + setsu/CMakeLists.txt | 24 +++++ setsu/cmake/FindUDEV.cmake | 26 ++++++ setsu/include/setsu.h | 26 ++++++ setsu/src/setsu.c | 177 +++++++++++++++++++++++++++++++++++++ setsu/test/main.c | 25 ++++++ 7 files changed, 281 insertions(+) create mode 100644 setsu/CMakeLists.txt create mode 100644 setsu/cmake/FindUDEV.cmake create mode 100644 setsu/include/setsu.h create mode 100644 setsu/src/setsu.c create mode 100644 setsu/test/main.c diff --git a/.gitignore b/.gitignore index 78d5d3a..5e82c02 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,5 @@ chiaki.rb *.jks secret.tar keystore-env.sh +compile_commands.json +.ccls-cache diff --git a/CMakeLists.txt b/CMakeLists.txt index 4c203ef..841e1d3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -65,6 +65,7 @@ if(CHIAKI_ENABLE_CLI) endif() if(CHIAKI_ENABLE_GUI) + add_subdirectory(setsu) add_subdirectory(gui) endif() diff --git a/setsu/CMakeLists.txt b/setsu/CMakeLists.txt new file mode 100644 index 0000000..e230c49 --- /dev/null +++ b/setsu/CMakeLists.txt @@ -0,0 +1,24 @@ + +cmake_minimum_required(VERSION 3.2) + +project(libsetsu) + +option(SETSU_BUILD_TEST "Build testing executable for libsetsu" OFF) + +list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake") + +add_library(setsu + include/setsu.h + src/setsu.c) + +target_include_directories(setsu PUBLIC include) + +find_package(UDEV REQUIRED) +target_link_libraries(setsu UDEV::libudev) + +if(SETSU_BUILD_TEST) + add_executable(setsutest + test/main.c) + target_link_libraries(setsutest setsu) +endif() + diff --git a/setsu/cmake/FindUDEV.cmake b/setsu/cmake/FindUDEV.cmake new file mode 100644 index 0000000..3b6b1e0 --- /dev/null +++ b/setsu/cmake/FindUDEV.cmake @@ -0,0 +1,26 @@ +# Provides: UDEV::libudev + +set(_prefix UDEV) +set(_target "${_prefix}::libudev") + +find_package(PkgConfig REQUIRED) + +pkg_check_modules("${_prefix}" libudev) + +function(resolve_location) + +endfunction() + +if(${_prefix}_FOUND) + add_library("${_target}" INTERFACE IMPORTED) + target_link_libraries("${_target}" INTERFACE ${${_prefix}_LIBRARIES}) + target_include_directories("${_target}" INTERFACE ${${_prefix}_INCLUDE_DIRS}) +endif() + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args("${_prefix}" + REQUIRED_VARS "${_prefix}_LIBRARIES" + VERSION_VAR "${_prefix}_VERSION") + +unset(_prefix) +unset(_target) diff --git a/setsu/include/setsu.h b/setsu/include/setsu.h new file mode 100644 index 0000000..8df7536 --- /dev/null +++ b/setsu/include/setsu.h @@ -0,0 +1,26 @@ +/* + * This file is part of Chiaki. + * + * Chiaki is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Chiaki is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Chiaki. If not, see . + */ + +#ifndef _SETSU_H +#define _SETSU_H + +typedef struct setsu_ctx_t SetsuCtx; + +SetsuCtx *setsu_ctx_new(); +void setsu_ctx_free(SetsuCtx *ctx); + +#endif diff --git a/setsu/src/setsu.c b/setsu/src/setsu.c new file mode 100644 index 0000000..76ae7d5 --- /dev/null +++ b/setsu/src/setsu.c @@ -0,0 +1,177 @@ + +#include + +#include + +#include +#include +#include + + +#include + +typedef struct setsu_device_t +{ + struct setsu_device_t *next; + char *path; + int fd; +} SetsuDevice; + +struct setsu_ctx_t +{ + struct udev *udev_ctx; + SetsuDevice *dev; +}; + + +static void scan(SetsuCtx *ctx); +static void update_device(SetsuCtx *ctx, struct udev_device *dev, bool added); +static SetsuDevice *connect(SetsuCtx *ctx, const char *path); +static void disconnect(SetsuCtx *ctx, SetsuDevice *dev); + +SetsuCtx *setsu_ctx_new() +{ + SetsuCtx *ctx = malloc(sizeof(SetsuCtx)); + if(!ctx) + return NULL; + + ctx->dev = NULL; + + ctx->udev_ctx = udev_new(); + if(!ctx->udev_ctx) + { + free(ctx); + return NULL; + } + + // TODO: monitor + + scan(ctx); + + return ctx; +} + +void setsu_ctx_free(SetsuCtx *ctx) +{ + while(ctx->dev) + disconnect(ctx, ctx->dev); + udev_unref(ctx->udev_ctx); + free(ctx); +} + +static void scan(SetsuCtx *ctx) +{ + struct udev_enumerate *udev_enum = udev_enumerate_new(ctx->udev_ctx); + if(!udev_enum) + return; + + if(udev_enumerate_add_match_subsystem(udev_enum, "input") < 0) + goto beach; + + if(udev_enumerate_add_match_property(udev_enum, "ID_INPUT_TOUCHPAD", "1") < 0) + goto beach; + + if(udev_enumerate_scan_devices(udev_enum) < 0) + goto beach; + + for(struct udev_list_entry *entry = udev_enumerate_get_list_entry(udev_enum); entry; entry = udev_list_entry_get_next(entry)) + { + const char *path = udev_list_entry_get_name(entry); + if(!path) + continue; + struct udev_device *dev = udev_device_new_from_syspath(ctx->udev_ctx, path); + if(!dev) + continue; + update_device(ctx, dev, true); + udev_device_unref(dev); + } + +beach: + udev_enumerate_unref(udev_enum); +} + +static bool is_device_interesting(struct udev_device *dev) +{ + static const char *device_ids[] = { + // vendor id, model id + "054c", "05c4", // DualShock 4 USB + NULL + }; + + // Filter mouse-device (/dev/input/mouse*) away and only keep the evdev (/dev/input/event*) one: + if(!udev_device_get_property_value(dev, "ID_INPUT_TOUCHPAD_INTEGRATION")) + return false; + + const char *vendor = udev_device_get_property_value(dev, "ID_VENDOR_ID"); + const char *model = udev_device_get_property_value(dev, "ID_MODEL_ID"); + if(!vendor || !model) + return false; + + for(const char **dev_id = device_ids; *dev_id; dev_id += 2) + { + if(!strcmp(vendor, dev_id[0]) && !strcmp(model, dev_id[1])) + return true; + } + return false; +} + +static void update_device(SetsuCtx *ctx, struct udev_device *dev, bool added) +{ + if(!is_device_interesting(dev)) + return; + const char *path = udev_device_get_devnode(dev); + if(!path) + return; + + for(SetsuDevice *dev = ctx->dev; dev; dev=dev->next) + { + if(!strcmp(dev->path, path)) + { + if(added) + return; // already added, do nothing + disconnect(ctx, dev); + } + } + connect(ctx, path); +} + +static SetsuDevice *connect(SetsuCtx *ctx, const char *path) +{ + SetsuDevice *dev = malloc(sizeof(SetsuDevice)); + if(!dev) + return NULL; + dev->path = strdup(path); + if(!dev->path) + { + free(dev); + return NULL; + } + + printf("connect %s\n", dev->path); + //dev->fd = open(dev->path, O_RDONLY | O_NONBLOCK); + + dev->next = ctx->dev; + ctx->dev = dev; + return dev; +} + +static void disconnect(SetsuCtx *ctx, SetsuDevice *dev) +{ + if(ctx->dev == dev) + ctx->dev = dev->next; + else + { + for(SetsuDevice *pdev = ctx->dev; pdev; pdev=pdev->next) + { + if(pdev->next == dev) + { + pdev->next = dev->next; + break; + } + } + } + + free(dev->path); + free(dev); +} + diff --git a/setsu/test/main.c b/setsu/test/main.c new file mode 100644 index 0000000..823c283 --- /dev/null +++ b/setsu/test/main.c @@ -0,0 +1,25 @@ + +#include + +#include +#include + +SetsuCtx *ctx; + +void quit() +{ + setsu_ctx_free(ctx); +} + +int main() +{ + ctx = setsu_ctx_new(); + if(!ctx) + { + printf("Failed to init setsu\n"); + return 1; + } + atexit(quit); + return 0; +} + From 15f2ad5142ca1d42363e58a88fef18e0af107639 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Sun, 21 Jun 2020 14:22:11 +0200 Subject: [PATCH 004/237] Temporarily disable libsetsu for full project --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 841e1d3..459002c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -65,7 +65,7 @@ if(CHIAKI_ENABLE_CLI) endif() if(CHIAKI_ENABLE_GUI) - add_subdirectory(setsu) + #add_subdirectory(setsu) add_subdirectory(gui) endif() From d9d89f712280844fb4fd69a2dd7ed7c664e6d963 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Sun, 21 Jun 2020 21:30:46 +0200 Subject: [PATCH 005/237] Get Events from evdev --- setsu/CMakeLists.txt | 5 +- setsu/cmake/FindEvdev.cmake | 26 ++++++++ .../cmake/{FindUDEV.cmake => FindUdev.cmake} | 4 +- setsu/include/setsu.h | 1 + setsu/src/setsu.c | 65 +++++++++++++++++-- setsu/test/main.c | 17 +++++ 6 files changed, 108 insertions(+), 10 deletions(-) create mode 100644 setsu/cmake/FindEvdev.cmake rename setsu/cmake/{FindUDEV.cmake => FindUdev.cmake} (92%) diff --git a/setsu/CMakeLists.txt b/setsu/CMakeLists.txt index e230c49..3c63a9f 100644 --- a/setsu/CMakeLists.txt +++ b/setsu/CMakeLists.txt @@ -13,8 +13,9 @@ add_library(setsu target_include_directories(setsu PUBLIC include) -find_package(UDEV REQUIRED) -target_link_libraries(setsu UDEV::libudev) +find_package(Udev REQUIRED) +find_package(Evdev REQUIRED) +target_link_libraries(setsu Udev::libudev Evdev::libevdev) if(SETSU_BUILD_TEST) add_executable(setsutest diff --git a/setsu/cmake/FindEvdev.cmake b/setsu/cmake/FindEvdev.cmake new file mode 100644 index 0000000..a6be138 --- /dev/null +++ b/setsu/cmake/FindEvdev.cmake @@ -0,0 +1,26 @@ +# Provides: Evdev::libevdev + +set(_prefix Evdev) +set(_target "${_prefix}::libevdev") + +find_package(PkgConfig REQUIRED) + +pkg_check_modules("${_prefix}" libevdev) + +function(resolve_location) + +endfunction() + +if(${_prefix}_FOUND) + add_library("${_target}" INTERFACE IMPORTED) + target_link_libraries("${_target}" INTERFACE ${${_prefix}_LIBRARIES}) + target_include_directories("${_target}" INTERFACE ${${_prefix}_INCLUDE_DIRS}) +endif() + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args("${_prefix}" + REQUIRED_VARS "${_prefix}_LIBRARIES" + VERSION_VAR "${_prefix}_VERSION") + +unset(_prefix) +unset(_target) diff --git a/setsu/cmake/FindUDEV.cmake b/setsu/cmake/FindUdev.cmake similarity index 92% rename from setsu/cmake/FindUDEV.cmake rename to setsu/cmake/FindUdev.cmake index 3b6b1e0..369f806 100644 --- a/setsu/cmake/FindUDEV.cmake +++ b/setsu/cmake/FindUdev.cmake @@ -1,6 +1,6 @@ -# Provides: UDEV::libudev +# Provides: Udev::libudev -set(_prefix UDEV) +set(_prefix Udev) set(_target "${_prefix}::libudev") find_package(PkgConfig REQUIRED) diff --git a/setsu/include/setsu.h b/setsu/include/setsu.h index 8df7536..5d8e766 100644 --- a/setsu/include/setsu.h +++ b/setsu/include/setsu.h @@ -22,5 +22,6 @@ typedef struct setsu_ctx_t SetsuCtx; SetsuCtx *setsu_ctx_new(); void setsu_ctx_free(SetsuCtx *ctx); +void setsu_ctx_run(SetsuCtx *ctx); #endif diff --git a/setsu/src/setsu.c b/setsu/src/setsu.c index 76ae7d5..5bd2681 100644 --- a/setsu/src/setsu.c +++ b/setsu/src/setsu.c @@ -1,11 +1,31 @@ +/* + * This file is part of Chiaki. + * + * Chiaki is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Chiaki is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Chiaki. If not, see . + */ #include #include +#include #include #include #include +#include +#include +#include #include @@ -15,6 +35,7 @@ typedef struct setsu_device_t struct setsu_device_t *next; char *path; int fd; + struct libevdev *evdev; } SetsuDevice; struct setsu_ctx_t @@ -140,19 +161,32 @@ static SetsuDevice *connect(SetsuCtx *ctx, const char *path) SetsuDevice *dev = malloc(sizeof(SetsuDevice)); if(!dev) return NULL; + memset(dev, 0, sizeof(*dev)); + dev->fd = -1; dev->path = strdup(path); if(!dev->path) - { - free(dev); - return NULL; - } + goto error; - printf("connect %s\n", dev->path); - //dev->fd = open(dev->path, O_RDONLY | O_NONBLOCK); + dev->fd = open(dev->path, O_RDONLY | O_NONBLOCK); + if(dev->fd == -1) + goto error; + + if(libevdev_new_from_fd(dev->fd, &dev->evdev) < 0) + goto error; + + printf("connected to %s\n", libevdev_get_name(dev->evdev)); dev->next = ctx->dev; ctx->dev = dev; return dev; +error: + if(dev->evdev) + libevdev_free(dev->evdev); + if(dev->fd != -1) + close(dev->fd); + free(dev->path); + free(dev); + return NULL; } static void disconnect(SetsuCtx *ctx, SetsuDevice *dev) @@ -175,3 +209,22 @@ static void disconnect(SetsuCtx *ctx, SetsuDevice *dev) free(dev); } +void setsu_ctx_run(SetsuCtx *ctx) +{ + SetsuDevice *dev = ctx->dev; + if(!dev) + return; + + int rc; + do + { + struct input_event ev; + rc = libevdev_next_event(dev->evdev, LIBEVDEV_READ_FLAG_NORMAL, &ev); + if (rc == 0) + printf("Event: %s %s %d\n", + libevdev_event_type_get_name(ev.type), + libevdev_event_code_get_name(ev.type, ev.code), + ev.value); + } while (rc == 1 || rc == 0 || rc == -EAGAIN); +} + diff --git a/setsu/test/main.c b/setsu/test/main.c index 823c283..bfbf0f7 100644 --- a/setsu/test/main.c +++ b/setsu/test/main.c @@ -1,3 +1,19 @@ +/* + * This file is part of Chiaki. + * + * Chiaki is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Chiaki is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Chiaki. If not, see . + */ #include @@ -19,6 +35,7 @@ int main() printf("Failed to init setsu\n"); return 1; } + setsu_ctx_run(ctx); atexit(quit); return 0; } From 919d3b1c3b8faccff298495d75a8d586ee1e4df5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Sat, 27 Jun 2020 12:30:13 +0200 Subject: [PATCH 006/237] Fix macOS on Travis (#265) --- .travis.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index ef3245a..7af6368 100644 --- a/.travis.yml +++ b/.travis.yml @@ -79,7 +79,6 @@ matrix: osx_image: xcode11 addons: homebrew: - update: true # tmp packages: - qt - opus @@ -91,8 +90,7 @@ matrix: - CMAKE_EXTRA_ARGS="" - DEPLOY=1 install: - - brew unlink python@2 # tmp - - brew link python # tmp + - pip3 install protobuf - scripts/build-ffmpeg.sh script: - scripts/travis-build.sh From 9c91843d9879bc29854fac5cc304ae8c23b4fc8c Mon Sep 17 00:00:00 2001 From: Kaiwen Xu Date: Sat, 27 Jun 2020 03:42:26 -0700 Subject: [PATCH 007/237] Expose videotoolbox hardware decoder (#261) --- gui/include/videodecoder.h | 2 ++ gui/src/settings.cpp | 3 ++- gui/src/settingsdialog.cpp | 3 ++- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/gui/include/videodecoder.h b/gui/include/videodecoder.h index 84ccf0a..9fa1d5b 100644 --- a/gui/include/videodecoder.h +++ b/gui/include/videodecoder.h @@ -38,6 +38,7 @@ typedef enum { HW_DECODE_NONE = 0, HW_DECODE_VAAPI = 1, HW_DECODE_VDPAU = 2, + HW_DECODE_VIDEOTOOLBOX = 3, } HardwareDecodeEngine; @@ -45,6 +46,7 @@ static const QMap hardware_decode_engine_nam { HW_DECODE_NONE, "none"}, { HW_DECODE_VAAPI, "vaapi"}, { HW_DECODE_VDPAU, "vdpau"}, + { HW_DECODE_VIDEOTOOLBOX, "videotoolbox"}, }; class VideoDecoderException: public Exception diff --git a/gui/src/settings.cpp b/gui/src/settings.cpp index 90a5c03..8b23f55 100644 --- a/gui/src/settings.cpp +++ b/gui/src/settings.cpp @@ -97,7 +97,8 @@ unsigned int Settings::GetAudioBufferSizeRaw() const static const QMap hw_decode_engine_values = { { HW_DECODE_NONE, "none" }, { HW_DECODE_VAAPI, "vaapi" }, - { HW_DECODE_VDPAU, "vdpau" } + { HW_DECODE_VDPAU, "vdpau" }, + { HW_DECODE_VIDEOTOOLBOX, "videotoolbox" } }; static const HardwareDecodeEngine hw_decode_engine_default = HW_DECODE_NONE; diff --git a/gui/src/settingsdialog.cpp b/gui/src/settingsdialog.cpp index 3b0892a..04352bc 100644 --- a/gui/src/settingsdialog.cpp +++ b/gui/src/settingsdialog.cpp @@ -158,7 +158,8 @@ SettingsDialog::SettingsDialog(Settings *settings, QWidget *parent) : QDialog(pa hardware_decode_combo_box = new QComboBox(this); static const QList> hardware_decode_engines = { { HW_DECODE_NONE, "none"}, - { HW_DECODE_VAAPI, "vaapi"} + { HW_DECODE_VAAPI, "vaapi"}, + { HW_DECODE_VIDEOTOOLBOX, "videotoolbox"} }; auto current_hardware_decode_engine = settings->GetHardwareDecodeEngine(); for(const auto &p : hardware_decode_engines) From 092355ff006adec596c3547495194e6166f07c0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Mon, 29 Jun 2020 22:28:42 +0200 Subject: [PATCH 008/237] Better Polling in Setsu --- setsu/include/setsu.h | 8 +-- setsu/src/setsu.c | 137 ++++++++++++++++++++++++++---------------- setsu/test/main.c | 11 ++-- 3 files changed, 94 insertions(+), 62 deletions(-) diff --git a/setsu/include/setsu.h b/setsu/include/setsu.h index 5d8e766..17439d0 100644 --- a/setsu/include/setsu.h +++ b/setsu/include/setsu.h @@ -18,10 +18,10 @@ #ifndef _SETSU_H #define _SETSU_H -typedef struct setsu_ctx_t SetsuCtx; +typedef struct setsu_t Setsu; -SetsuCtx *setsu_ctx_new(); -void setsu_ctx_free(SetsuCtx *ctx); -void setsu_ctx_run(SetsuCtx *ctx); +Setsu *setsu_new(); +void setsu_free(Setsu *setsu); +void setsu_poll(Setsu *setsu); #endif diff --git a/setsu/src/setsu.c b/setsu/src/setsu.c index 5bd2681..8be00c4 100644 --- a/setsu/src/setsu.c +++ b/setsu/src/setsu.c @@ -30,6 +30,10 @@ #include + +#define SETSU_LOG(...) fprintf(stderr, __VA_ARGS__) + + typedef struct setsu_device_t { struct setsu_device_t *next; @@ -38,51 +42,53 @@ typedef struct setsu_device_t struct libevdev *evdev; } SetsuDevice; -struct setsu_ctx_t +struct setsu_t { - struct udev *udev_ctx; + struct udev *udev_setsu; SetsuDevice *dev; }; -static void scan(SetsuCtx *ctx); -static void update_device(SetsuCtx *ctx, struct udev_device *dev, bool added); -static SetsuDevice *connect(SetsuCtx *ctx, const char *path); -static void disconnect(SetsuCtx *ctx, SetsuDevice *dev); +static void scan(Setsu *setsu); +static void update_device(Setsu *setsu, struct udev_device *dev, bool added); +static SetsuDevice *connect(Setsu *setsu, const char *path); +static void disconnect(Setsu *setsu, SetsuDevice *dev); +static void poll_device(Setsu *setsu, SetsuDevice *dev); +static void device_event(Setsu *setsu, SetsuDevice *dev, struct input_event *ev); -SetsuCtx *setsu_ctx_new() +Setsu *setsu_new() { - SetsuCtx *ctx = malloc(sizeof(SetsuCtx)); - if(!ctx) + Setsu *setsu = malloc(sizeof(Setsu)); + if(!setsu) return NULL; - ctx->dev = NULL; + setsu->dev = NULL; - ctx->udev_ctx = udev_new(); - if(!ctx->udev_ctx) + setsu->udev_setsu = udev_new(); + if(!setsu->udev_setsu) { - free(ctx); + free(setsu); return NULL; } // TODO: monitor - scan(ctx); + scan(setsu); - return ctx; + return setsu; } -void setsu_ctx_free(SetsuCtx *ctx) +void setsu_free(Setsu *setsu) { - while(ctx->dev) - disconnect(ctx, ctx->dev); - udev_unref(ctx->udev_ctx); - free(ctx); + while(setsu->dev) + disconnect(setsu, setsu->dev); + udev_unref(setsu->udev_setsu); + free(setsu); } -static void scan(SetsuCtx *ctx) +static void scan(Setsu *setsu) { - struct udev_enumerate *udev_enum = udev_enumerate_new(ctx->udev_ctx); + struct udev_enumerate *udev_enum = udev_enumerate_new(setsu->udev_setsu); if(!udev_enum) return; @@ -100,10 +106,11 @@ static void scan(SetsuCtx *ctx) const char *path = udev_list_entry_get_name(entry); if(!path) continue; - struct udev_device *dev = udev_device_new_from_syspath(ctx->udev_ctx, path); + struct udev_device *dev = udev_device_new_from_syspath(setsu->udev_setsu, path); if(!dev) continue; - update_device(ctx, dev, true); + SETSU_LOG("enum device: %s\n", path); + update_device(setsu, dev, true); udev_device_unref(dev); } @@ -115,7 +122,8 @@ static bool is_device_interesting(struct udev_device *dev) { static const char *device_ids[] = { // vendor id, model id - "054c", "05c4", // DualShock 4 USB + "054c", "05c4", // DualShock 4 Gen 1 USB + "054c", "09cc", // DualShock 4 Gen 2 USB NULL }; @@ -136,7 +144,7 @@ static bool is_device_interesting(struct udev_device *dev) return false; } -static void update_device(SetsuCtx *ctx, struct udev_device *dev, bool added) +static void update_device(Setsu *setsu, struct udev_device *dev, bool added) { if(!is_device_interesting(dev)) return; @@ -144,19 +152,19 @@ static void update_device(SetsuCtx *ctx, struct udev_device *dev, bool added) if(!path) return; - for(SetsuDevice *dev = ctx->dev; dev; dev=dev->next) + for(SetsuDevice *dev = setsu->dev; dev; dev=dev->next) { if(!strcmp(dev->path, path)) { if(added) return; // already added, do nothing - disconnect(ctx, dev); + disconnect(setsu, dev); } } - connect(ctx, path); + connect(setsu, path); } -static SetsuDevice *connect(SetsuCtx *ctx, const char *path) +static SetsuDevice *connect(Setsu *setsu, const char *path) { SetsuDevice *dev = malloc(sizeof(SetsuDevice)); if(!dev) @@ -174,10 +182,10 @@ static SetsuDevice *connect(SetsuCtx *ctx, const char *path) if(libevdev_new_from_fd(dev->fd, &dev->evdev) < 0) goto error; - printf("connected to %s\n", libevdev_get_name(dev->evdev)); + SETSU_LOG("connected to %s\n", libevdev_get_name(dev->evdev)); - dev->next = ctx->dev; - ctx->dev = dev; + dev->next = setsu->dev; + setsu->dev = dev; return dev; error: if(dev->evdev) @@ -189,13 +197,13 @@ error: return NULL; } -static void disconnect(SetsuCtx *ctx, SetsuDevice *dev) +static void disconnect(Setsu *setsu, SetsuDevice *dev) { - if(ctx->dev == dev) - ctx->dev = dev->next; + if(setsu->dev == dev) + setsu->dev = dev->next; else { - for(SetsuDevice *pdev = ctx->dev; pdev; pdev=pdev->next) + for(SetsuDevice *pdev = setsu->dev; pdev; pdev=pdev->next) { if(pdev->next == dev) { @@ -209,22 +217,45 @@ static void disconnect(SetsuCtx *ctx, SetsuDevice *dev) free(dev); } -void setsu_ctx_run(SetsuCtx *ctx) +void setsu_poll(Setsu *setsu) { - SetsuDevice *dev = ctx->dev; - if(!dev) - return; - - int rc; - do - { - struct input_event ev; - rc = libevdev_next_event(dev->evdev, LIBEVDEV_READ_FLAG_NORMAL, &ev); - if (rc == 0) - printf("Event: %s %s %d\n", - libevdev_event_type_get_name(ev.type), - libevdev_event_code_get_name(ev.type, ev.code), - ev.value); - } while (rc == 1 || rc == 0 || rc == -EAGAIN); + for(SetsuDevice *dev = setsu->dev; dev; dev = dev->next) + poll_device(setsu, dev); +} + +static void poll_device(Setsu *setsu, SetsuDevice *dev) +{ + bool sync = false; + while(true) + { + struct input_event ev; + int r = libevdev_next_event(dev->evdev, sync ? LIBEVDEV_READ_FLAG_SYNC : LIBEVDEV_READ_FLAG_NORMAL, &ev); + if(r == -EAGAIN) + { + if(sync) + { + sync = false; + continue; + } + break; + } + if(r == LIBEVDEV_READ_STATUS_SUCCESS || (sync && r == LIBEVDEV_READ_STATUS_SYNC)) + device_event(setsu, dev, &ev); + else if(r == LIBEVDEV_READ_STATUS_SYNC) + sync = true; + else + { + SETSU_LOG("evdev poll failed\n"); + break; + } + } +} + +static void device_event(Setsu *setsu, SetsuDevice *dev, struct input_event *ev) +{ + printf("Event: %s %s %d\n", + libevdev_event_type_get_name(ev->type), + libevdev_event_code_get_name(ev->type, ev->code), + ev->value); } diff --git a/setsu/test/main.c b/setsu/test/main.c index bfbf0f7..fa1c137 100644 --- a/setsu/test/main.c +++ b/setsu/test/main.c @@ -20,22 +20,23 @@ #include #include -SetsuCtx *ctx; +Setsu *setsu; void quit() { - setsu_ctx_free(ctx); + setsu_free(setsu); } int main() { - ctx = setsu_ctx_new(); - if(!ctx) + setsu = setsu_new(); + if(!setsu) { printf("Failed to init setsu\n"); return 1; } - setsu_ctx_run(ctx); + while(1) + setsu_poll(setsu); atexit(quit); return 0; } From aabb305eef6d50a859f852e18701f1bb1c54b09b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Mon, 29 Jun 2020 23:58:26 +0200 Subject: [PATCH 009/237] Add some ugly events to setsu --- setsu/include/setsu.h | 19 +++++++- setsu/src/setsu.c | 78 +++++++++++++++++++++++++------ setsu/test/main.c | 106 +++++++++++++++++++++++++++++++++++++++++- 3 files changed, 187 insertions(+), 16 deletions(-) diff --git a/setsu/include/setsu.h b/setsu/include/setsu.h index 17439d0..aecf352 100644 --- a/setsu/include/setsu.h +++ b/setsu/include/setsu.h @@ -19,9 +19,26 @@ #define _SETSU_H typedef struct setsu_t Setsu; +typedef struct setsu_device_t SetsuDevice; + +typedef enum { + SETSU_EVENT_DOWN, + SETSU_EVENT_UP, + SETSU_EVENT_POSITION_X, + SETSU_EVENT_POSITION_Y +} SetsuEventType; + +typedef struct setsu_event_t { + SetsuDevice *dev; + unsigned int tracking_id; + SetsuEventType type; + unsigned int value; +} SetsuEvent; + +typedef void (*SetsuEventCb)(SetsuEvent *event, void *user); Setsu *setsu_new(); void setsu_free(Setsu *setsu); -void setsu_poll(Setsu *setsu); +void setsu_poll(Setsu *setsu, SetsuEventCb cb); #endif diff --git a/setsu/src/setsu.c b/setsu/src/setsu.c index 8be00c4..8a09455 100644 --- a/setsu/src/setsu.c +++ b/setsu/src/setsu.c @@ -33,6 +33,7 @@ #define SETSU_LOG(...) fprintf(stderr, __VA_ARGS__) +#define SLOTS_COUNT 16 typedef struct setsu_device_t { @@ -40,6 +41,12 @@ typedef struct setsu_device_t char *path; int fd; struct libevdev *evdev; + + struct { + bool down; + unsigned int tracking_id; + } slots[SLOTS_COUNT]; + unsigned int slot_cur; } SetsuDevice; struct setsu_t @@ -53,8 +60,8 @@ static void scan(Setsu *setsu); static void update_device(Setsu *setsu, struct udev_device *dev, bool added); static SetsuDevice *connect(Setsu *setsu, const char *path); static void disconnect(Setsu *setsu, SetsuDevice *dev); -static void poll_device(Setsu *setsu, SetsuDevice *dev); -static void device_event(Setsu *setsu, SetsuDevice *dev, struct input_event *ev); +static void poll_device(Setsu *setsu, SetsuDevice *dev, SetsuEventCb cb); +static void device_event(Setsu *setsu, SetsuDevice *dev, struct input_event *ev, SetsuEventCb cb); Setsu *setsu_new() { @@ -166,10 +173,9 @@ static void update_device(Setsu *setsu, struct udev_device *dev, bool added) static SetsuDevice *connect(Setsu *setsu, const char *path) { - SetsuDevice *dev = malloc(sizeof(SetsuDevice)); + SetsuDevice *dev = calloc(1, sizeof(SetsuDevice)); if(!dev) return NULL; - memset(dev, 0, sizeof(*dev)); dev->fd = -1; dev->path = strdup(path); if(!dev->path) @@ -182,7 +188,12 @@ static SetsuDevice *connect(Setsu *setsu, const char *path) if(libevdev_new_from_fd(dev->fd, &dev->evdev) < 0) goto error; - SETSU_LOG("connected to %s\n", libevdev_get_name(dev->evdev)); + // TODO: expose these values: + int min_x = libevdev_get_abs_minimum(dev->evdev, ABS_X); + int min_y = libevdev_get_abs_minimum(dev->evdev, ABS_Y); + int max_x = libevdev_get_abs_maximum(dev->evdev, ABS_X); + int max_y = libevdev_get_abs_maximum(dev->evdev, ABS_Y); + SETSU_LOG("connected to %s: %d %d -> %d %d\n", libevdev_get_name(dev->evdev), min_x, min_y, max_x, max_y); dev->next = setsu->dev; setsu->dev = dev; @@ -217,13 +228,13 @@ static void disconnect(Setsu *setsu, SetsuDevice *dev) free(dev); } -void setsu_poll(Setsu *setsu) +void setsu_poll(Setsu *setsu, SetsuEventCb cb) { for(SetsuDevice *dev = setsu->dev; dev; dev = dev->next) - poll_device(setsu, dev); + poll_device(setsu, dev, cb); } -static void poll_device(Setsu *setsu, SetsuDevice *dev) +static void poll_device(Setsu *setsu, SetsuDevice *dev, SetsuEventCb cb) { bool sync = false; while(true) @@ -240,7 +251,7 @@ static void poll_device(Setsu *setsu, SetsuDevice *dev) break; } if(r == LIBEVDEV_READ_STATUS_SUCCESS || (sync && r == LIBEVDEV_READ_STATUS_SYNC)) - device_event(setsu, dev, &ev); + device_event(setsu, dev, &ev, cb); else if(r == LIBEVDEV_READ_STATUS_SYNC) sync = true; else @@ -251,11 +262,50 @@ static void poll_device(Setsu *setsu, SetsuDevice *dev) } } -static void device_event(Setsu *setsu, SetsuDevice *dev, struct input_event *ev) +static void send_event(Setsu *setsu, SetsuDevice *dev, SetsuEventCb cb, SetsuEventType type, unsigned int value) { - printf("Event: %s %s %d\n", - libevdev_event_type_get_name(ev->type), - libevdev_event_code_get_name(ev->type, ev->code), - ev->value); + SetsuEvent event; + event.dev = dev; + event.tracking_id = dev->slots[dev->slot_cur].tracking_id; + event.type = type; + event.value = value; + cb(&event, NULL); +} + +static void device_event(Setsu *setsu, SetsuDevice *dev, struct input_event *ev, SetsuEventCb cb) +{ + if(ev->type == EV_ABS) + { + switch(ev->code) + { + case ABS_MT_SLOT: + if((unsigned int)ev->value >= SLOTS_COUNT) + { + SETSU_LOG("slot too high\n"); + break; + } + dev->slot_cur = ev->value; + break; + case ABS_MT_TRACKING_ID: + if(ev->value == -1) + { + dev->slots[dev->slot_cur].down = false; + send_event(setsu, dev, cb, SETSU_EVENT_UP, 0); + } + else + { + dev->slots[dev->slot_cur].down = true; + dev->slots[dev->slot_cur].tracking_id = ev->value; + send_event(setsu, dev, cb, SETSU_EVENT_DOWN, 0); + } + break; + case ABS_MT_POSITION_X: + send_event(setsu, dev, cb, SETSU_EVENT_POSITION_X, ev->value); + break; + case ABS_MT_POSITION_Y: + send_event(setsu, dev, cb, SETSU_EVENT_POSITION_Y, ev->value); + break; + } + } } diff --git a/setsu/test/main.c b/setsu/test/main.c index fa1c137..bed5096 100644 --- a/setsu/test/main.c +++ b/setsu/test/main.c @@ -19,24 +19,128 @@ #include #include +#include +#include +#include Setsu *setsu; +#define WIDTH 1920 +#define HEIGHT 942 +#define TOUCHES_MAX 8 + +struct { + bool down; + unsigned int tracking_id; + unsigned int x, y; +} touches[TOUCHES_MAX]; + +bool dirty = false; + void quit() { setsu_free(setsu); } +void print_state() +{ + for(size_t i=0; itype) + { + case SETSU_EVENT_DOWN: + for(size_t i=0; itracking_id; + break; + } + } + break; + case SETSU_EVENT_POSITION_X: + case SETSU_EVENT_POSITION_Y: + case SETSU_EVENT_UP: + for(size_t i=0; itracking_id) + { + switch(event->type) + { + case SETSU_EVENT_POSITION_X: + touches[i].x = event->value; + break; + case SETSU_EVENT_POSITION_Y: + touches[i].y = event->value; + break; + case SETSU_EVENT_UP: + touches[i].down = false; + break; + default: + break; + } + } + } + break; + } +} + int main() { + memset(touches, 0, sizeof(touches)); setsu = setsu_new(); if(!setsu) { printf("Failed to init setsu\n"); return 1; } + dirty = true; while(1) - setsu_poll(setsu); + { + if(dirty) + print_state(); + dirty = false; + setsu_poll(setsu, event); + } atexit(quit); return 0; } From 43dd40e46659dba4c76eba27a793f2be9354e34d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Tue, 30 Jun 2020 10:31:18 +0200 Subject: [PATCH 010/237] Make setsu events nicer --- setsu/include/setsu.h | 12 +++-- setsu/src/setsu.c | 105 ++++++++++++++++++++++++++++++------------ setsu/test/main.c | 13 ++---- 3 files changed, 87 insertions(+), 43 deletions(-) diff --git a/setsu/include/setsu.h b/setsu/include/setsu.h index aecf352..de64970 100644 --- a/setsu/include/setsu.h +++ b/setsu/include/setsu.h @@ -18,27 +18,29 @@ #ifndef _SETSU_H #define _SETSU_H +#include + typedef struct setsu_t Setsu; typedef struct setsu_device_t SetsuDevice; +typedef int SetsuTrackingId; typedef enum { SETSU_EVENT_DOWN, SETSU_EVENT_UP, - SETSU_EVENT_POSITION_X, - SETSU_EVENT_POSITION_Y + SETSU_EVENT_POSITION } SetsuEventType; typedef struct setsu_event_t { SetsuDevice *dev; - unsigned int tracking_id; + SetsuTrackingId tracking_id; SetsuEventType type; - unsigned int value; + uint32_t x, y; } SetsuEvent; typedef void (*SetsuEventCb)(SetsuEvent *event, void *user); Setsu *setsu_new(); void setsu_free(Setsu *setsu); -void setsu_poll(Setsu *setsu, SetsuEventCb cb); +void setsu_poll(Setsu *setsu, SetsuEventCb cb, void *user); #endif diff --git a/setsu/src/setsu.c b/setsu/src/setsu.c index 8a09455..f43a3cc 100644 --- a/setsu/src/setsu.c +++ b/setsu/src/setsu.c @@ -43,8 +43,17 @@ typedef struct setsu_device_t struct libevdev *evdev; struct { - bool down; - unsigned int tracking_id; + /* Saves the old tracking id that was just up-ed. + * also for handling "atomic" up->down + * i.e. when there is an up, then down with a different tracking id + * in a single frame (before SYN_REPORT), this saves the old + * tracking id that must be reported as up. */ + int tracking_id_prev; + + int tracking_id; + int x, y; + bool downed; + bool pos_dirty; } slots[SLOTS_COUNT]; unsigned int slot_cur; } SetsuDevice; @@ -60,8 +69,9 @@ static void scan(Setsu *setsu); static void update_device(Setsu *setsu, struct udev_device *dev, bool added); static SetsuDevice *connect(Setsu *setsu, const char *path); static void disconnect(Setsu *setsu, SetsuDevice *dev); -static void poll_device(Setsu *setsu, SetsuDevice *dev, SetsuEventCb cb); -static void device_event(Setsu *setsu, SetsuDevice *dev, struct input_event *ev, SetsuEventCb cb); +static void poll_device(Setsu *setsu, SetsuDevice *dev, SetsuEventCb cb, void *user); +static void device_event(Setsu *setsu, SetsuDevice *dev, struct input_event *ev, SetsuEventCb cb, void *user); +static void device_drain(Setsu *setsu, SetsuDevice *dev, SetsuEventCb cb, void *user); Setsu *setsu_new() { @@ -228,13 +238,13 @@ static void disconnect(Setsu *setsu, SetsuDevice *dev) free(dev); } -void setsu_poll(Setsu *setsu, SetsuEventCb cb) +void setsu_poll(Setsu *setsu, SetsuEventCb cb, void *user) { for(SetsuDevice *dev = setsu->dev; dev; dev = dev->next) - poll_device(setsu, dev, cb); + poll_device(setsu, dev, cb, user); } -static void poll_device(Setsu *setsu, SetsuDevice *dev, SetsuEventCb cb) +static void poll_device(Setsu *setsu, SetsuDevice *dev, SetsuEventCb cb, void *user) { bool sync = false; while(true) @@ -251,7 +261,7 @@ static void poll_device(Setsu *setsu, SetsuDevice *dev, SetsuEventCb cb) break; } if(r == LIBEVDEV_READ_STATUS_SUCCESS || (sync && r == LIBEVDEV_READ_STATUS_SYNC)) - device_event(setsu, dev, &ev, cb); + device_event(setsu, dev, &ev, cb, user); else if(r == LIBEVDEV_READ_STATUS_SYNC) sync = true; else @@ -262,18 +272,15 @@ static void poll_device(Setsu *setsu, SetsuDevice *dev, SetsuEventCb cb) } } -static void send_event(Setsu *setsu, SetsuDevice *dev, SetsuEventCb cb, SetsuEventType type, unsigned int value) -{ - SetsuEvent event; - event.dev = dev; - event.tracking_id = dev->slots[dev->slot_cur].tracking_id; - event.type = type; - event.value = value; - cb(&event, NULL); -} - -static void device_event(Setsu *setsu, SetsuDevice *dev, struct input_event *ev, SetsuEventCb cb) +static void device_event(Setsu *setsu, SetsuDevice *dev, struct input_event *ev, SetsuEventCb cb, void *user) { +#if 0 + SETSU_LOG("Event: %s %s %d\n", + libevdev_event_type_get_name(ev->type), + libevdev_event_code_get_name(ev->type, ev->code), + ev->value); +#endif +#define S dev->slots[dev->slot_cur] if(ev->type == EV_ABS) { switch(ev->code) @@ -287,25 +294,63 @@ static void device_event(Setsu *setsu, SetsuDevice *dev, struct input_event *ev, dev->slot_cur = ev->value; break; case ABS_MT_TRACKING_ID: - if(ev->value == -1) + if(S.tracking_id != -1 && S.tracking_id_prev == -1) { - dev->slots[dev->slot_cur].down = false; - send_event(setsu, dev, cb, SETSU_EVENT_UP, 0); - } - else - { - dev->slots[dev->slot_cur].down = true; - dev->slots[dev->slot_cur].tracking_id = ev->value; - send_event(setsu, dev, cb, SETSU_EVENT_DOWN, 0); + // up the tracking id + S.tracking_id_prev = S.tracking_id; + // reset the rest + S.x = S.y = 0; + S.pos_dirty = false; } + S.tracking_id = ev->value; + S.downed = true; break; case ABS_MT_POSITION_X: - send_event(setsu, dev, cb, SETSU_EVENT_POSITION_X, ev->value); + S.x = ev->value; + S.pos_dirty = true; break; case ABS_MT_POSITION_Y: - send_event(setsu, dev, cb, SETSU_EVENT_POSITION_Y, ev->value); + S.y = ev->value; + S.pos_dirty = true; break; } } + else if(ev->type == EV_SYN && ev->code == SYN_REPORT) + device_drain(setsu, dev, cb, user); +} + +static void device_drain(Setsu *setsu, SetsuDevice *dev, SetsuEventCb cb, void *user) +{ + SetsuEvent event; +#define BEGIN_EVENT(tp) do { memset(&event, 0, sizeof(event)); event.dev = dev; event.type = tp; } while(0) +#define SEND_EVENT() do { cb(&event, user); } while (0) + for(size_t i=0; islots[i].tracking_id_prev != -1) + { + BEGIN_EVENT(SETSU_EVENT_UP); + event.tracking_id = dev->slots[i].tracking_id_prev; + SEND_EVENT(); + dev->slots[i].tracking_id_prev = -1; + } + if(dev->slots[i].downed) + { + BEGIN_EVENT(SETSU_EVENT_DOWN); + event.tracking_id = dev->slots[i].tracking_id; + SEND_EVENT(); + dev->slots[i].downed = false; + } + if(dev->slots[i].pos_dirty) + { + BEGIN_EVENT(SETSU_EVENT_POSITION); + event.tracking_id = dev->slots[i].tracking_id; + event.x = dev->slots[i].x; + event.y = dev->slots[i].y; + SEND_EVENT(); + dev->slots[i].pos_dirty = false; + } + } +#undef BEGIN_EVENT +#undef SEND_EVENT } diff --git a/setsu/test/main.c b/setsu/test/main.c index bed5096..6bc5882 100644 --- a/setsu/test/main.c +++ b/setsu/test/main.c @@ -97,8 +97,7 @@ void event(SetsuEvent *event, void *user) } } break; - case SETSU_EVENT_POSITION_X: - case SETSU_EVENT_POSITION_Y: + case SETSU_EVENT_POSITION: case SETSU_EVENT_UP: for(size_t i=0; itype) { - case SETSU_EVENT_POSITION_X: - touches[i].x = event->value; - break; - case SETSU_EVENT_POSITION_Y: - touches[i].y = event->value; + case SETSU_EVENT_POSITION: + touches[i].x = event->x; + touches[i].y = event->y; break; case SETSU_EVENT_UP: touches[i].down = false; @@ -139,7 +136,7 @@ int main() if(dirty) print_state(); dirty = false; - setsu_poll(setsu, event); + setsu_poll(setsu, event, NULL); } atexit(quit); return 0; From 4a1ad05b5dde9b607fa09dd1c128030596f38a3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Wed, 1 Jul 2020 18:44:54 +0200 Subject: [PATCH 011/237] Device Events in Setsu --- setsu/include/setsu.h | 37 +++++++++-- setsu/src/setsu.c | 151 ++++++++++++++++++++++++++++++++---------- setsu/test/main.c | 5 ++ 3 files changed, 155 insertions(+), 38 deletions(-) diff --git a/setsu/include/setsu.h b/setsu/include/setsu.h index de64970..eaf32a6 100644 --- a/setsu/include/setsu.h +++ b/setsu/include/setsu.h @@ -25,16 +25,43 @@ typedef struct setsu_device_t SetsuDevice; typedef int SetsuTrackingId; typedef enum { + /* New device available to connect. + * Event will have path set to the new device. */ + SETSU_EVENT_DEVICE_ADDED, + + /* Previously available device removed. + * Event will have path set to the new device. + * Any SetsuDevice connected to this path will automatically + * be disconnected and their pointers will be invalid immediately + * after the callback for this event returns. */ + SETSU_EVENT_DEVICE_REMOVED, + + /* Touch down. + * Event will have dev and tracking_id set. */ SETSU_EVENT_DOWN, + + /* Touch down. + * Event will have dev and tracking_id set. */ SETSU_EVENT_UP, + + /* Touch position update. + * Event will have dev, tracking_id, x and y set. */ SETSU_EVENT_POSITION } SetsuEventType; -typedef struct setsu_event_t { - SetsuDevice *dev; - SetsuTrackingId tracking_id; +typedef struct setsu_event_t +{ SetsuEventType type; - uint32_t x, y; + union + { + const char *path; + struct + { + SetsuDevice *dev; + SetsuTrackingId tracking_id; + uint32_t x, y; + }; + }; } SetsuEvent; typedef void (*SetsuEventCb)(SetsuEvent *event, void *user); @@ -42,5 +69,7 @@ typedef void (*SetsuEventCb)(SetsuEvent *event, void *user); Setsu *setsu_new(); void setsu_free(Setsu *setsu); void setsu_poll(Setsu *setsu, SetsuEventCb cb, void *user); +SetsuDevice *setsu_connect(Setsu *setsu, const char *path); +void setsu_disconnect(Setsu *setsu, SetsuDevice *dev); #endif diff --git a/setsu/src/setsu.c b/setsu/src/setsu.c index f43a3cc..56682fb 100644 --- a/setsu/src/setsu.c +++ b/setsu/src/setsu.c @@ -27,12 +27,18 @@ #include #include - #include - #define SETSU_LOG(...) fprintf(stderr, __VA_ARGS__) +typedef struct setsu_avail_device_t +{ + struct setsu_avail_device_t *next; + char *path; + bool connect_dirty; // whether the connect has not been sent as an event yet + bool disconnect_dirty; // whether the disconnect has not been sent as an event yet +} SetsuAvailDevice; + #define SLOTS_COUNT 16 typedef struct setsu_device_t @@ -41,6 +47,7 @@ typedef struct setsu_device_t char *path; int fd; struct libevdev *evdev; + int min_x, min_y, max_x, max_y; struct { /* Saves the old tracking id that was just up-ed. @@ -60,13 +67,13 @@ typedef struct setsu_device_t struct setsu_t { - struct udev *udev_setsu; + struct udev *udev; + SetsuAvailDevice *avail_dev; SetsuDevice *dev; }; - -static void scan(Setsu *setsu); -static void update_device(Setsu *setsu, struct udev_device *dev, bool added); +static void scan_udev(Setsu *setsu); +static void update_udev_device(Setsu *setsu, struct udev_device *dev, bool added); static SetsuDevice *connect(Setsu *setsu, const char *path); static void disconnect(Setsu *setsu, SetsuDevice *dev); static void poll_device(Setsu *setsu, SetsuDevice *dev, SetsuEventCb cb, void *user); @@ -75,14 +82,12 @@ static void device_drain(Setsu *setsu, SetsuDevice *dev, SetsuEventCb cb, void * Setsu *setsu_new() { - Setsu *setsu = malloc(sizeof(Setsu)); + Setsu *setsu = calloc(1, sizeof(Setsu)); if(!setsu) return NULL; - setsu->dev = NULL; - - setsu->udev_setsu = udev_new(); - if(!setsu->udev_setsu) + setsu->udev = udev_new(); + if(!setsu->udev) { free(setsu); return NULL; @@ -90,7 +95,7 @@ Setsu *setsu_new() // TODO: monitor - scan(setsu); + scan_udev(setsu); return setsu; } @@ -98,14 +103,14 @@ Setsu *setsu_new() void setsu_free(Setsu *setsu) { while(setsu->dev) - disconnect(setsu, setsu->dev); - udev_unref(setsu->udev_setsu); + setsu_disconnect(setsu, setsu->dev); + udev_unref(setsu->udev); free(setsu); } -static void scan(Setsu *setsu) +static void scan_udev(Setsu *setsu) { - struct udev_enumerate *udev_enum = udev_enumerate_new(setsu->udev_setsu); + struct udev_enumerate *udev_enum = udev_enumerate_new(setsu->udev); if(!udev_enum) return; @@ -123,11 +128,11 @@ static void scan(Setsu *setsu) const char *path = udev_list_entry_get_name(entry); if(!path) continue; - struct udev_device *dev = udev_device_new_from_syspath(setsu->udev_setsu, path); + struct udev_device *dev = udev_device_new_from_syspath(setsu->udev, path); if(!dev) continue; SETSU_LOG("enum device: %s\n", path); - update_device(setsu, dev, true); + update_udev_device(setsu, dev, true); udev_device_unref(dev); } @@ -161,7 +166,7 @@ static bool is_device_interesting(struct udev_device *dev) return false; } -static void update_device(Setsu *setsu, struct udev_device *dev, bool added) +static void update_udev_device(Setsu *setsu, struct udev_device *dev, bool added) { if(!is_device_interesting(dev)) return; @@ -169,19 +174,32 @@ static void update_device(Setsu *setsu, struct udev_device *dev, bool added) if(!path) return; - for(SetsuDevice *dev = setsu->dev; dev; dev=dev->next) + for(SetsuAvailDevice *adev = setsu->avail_dev; adev; adev=adev->next) { - if(!strcmp(dev->path, path)) + if(!strcmp(adev->path, path)) { if(added) return; // already added, do nothing - disconnect(setsu, dev); + // disconnected + adev->disconnect_dirty = true; } } - connect(setsu, path); + // not yet added + SetsuAvailDevice *adev = calloc(1, sizeof(SetsuAvailDevice)); + if(!adev) + return; + adev->path = strdup(path); + if(!adev->path) + { + free(adev); + return; + } + adev->next = setsu->avail_dev; + setsu->avail_dev = adev; + adev->connect_dirty = true; } -static SetsuDevice *connect(Setsu *setsu, const char *path) +SetsuDevice *setsu_connect(Setsu *setsu, const char *path) { SetsuDevice *dev = calloc(1, sizeof(SetsuDevice)); if(!dev) @@ -196,14 +214,21 @@ static SetsuDevice *connect(Setsu *setsu, const char *path) goto error; if(libevdev_new_from_fd(dev->fd, &dev->evdev) < 0) + { + dev->evdev = NULL; goto error; + } - // TODO: expose these values: - int min_x = libevdev_get_abs_minimum(dev->evdev, ABS_X); - int min_y = libevdev_get_abs_minimum(dev->evdev, ABS_Y); - int max_x = libevdev_get_abs_maximum(dev->evdev, ABS_X); - int max_y = libevdev_get_abs_maximum(dev->evdev, ABS_Y); - SETSU_LOG("connected to %s: %d %d -> %d %d\n", libevdev_get_name(dev->evdev), min_x, min_y, max_x, max_y); + dev->min_x = libevdev_get_abs_minimum(dev->evdev, ABS_X); + dev->min_y = libevdev_get_abs_minimum(dev->evdev, ABS_Y); + dev->max_x = libevdev_get_abs_maximum(dev->evdev, ABS_X); + dev->max_y = libevdev_get_abs_maximum(dev->evdev, ABS_Y); + + for(size_t i=0; islots[i].tracking_id_prev = -1; + dev->slots[i].tracking_id = -1; + } dev->next = setsu->dev; setsu->dev = dev; @@ -218,13 +243,13 @@ error: return NULL; } -static void disconnect(Setsu *setsu, SetsuDevice *dev) +void setsu_disconnect(Setsu *setsu, SetsuDevice *dev) { if(setsu->dev == dev) setsu->dev = dev->next; else { - for(SetsuDevice *pdev = setsu->dev; pdev; pdev=pdev->next) + for(SetsuDevice *pdev = setsu->dev; pdev; pdev = pdev->next) { if(pdev->next == dev) { @@ -233,13 +258,69 @@ static void disconnect(Setsu *setsu, SetsuDevice *dev) } } } - + libevdev_free(dev->evdev); + close(dev->fd); free(dev->path); free(dev); } +void kill_avail_device(Setsu *setsu, SetsuAvailDevice *adev) +{ + for(SetsuDevice *dev = setsu->dev; dev;) + { + if(!strcmp(dev->path, adev->path)) + { + SetsuDevice *next = dev->next; + setsu_disconnect(setsu, dev); + dev = next; + } + dev = dev->next; + } + + if(setsu->avail_dev == adev) + setsu->avail_dev = adev->next; + else + { + for(SetsuAvailDevice *padev = setsu->avail_dev; padev; padev = padev->next) + { + if(padev->next == adev) + { + padev->next = adev->next; + break; + } + } + } + free(adev->path); + free(adev); +} + void setsu_poll(Setsu *setsu, SetsuEventCb cb, void *user) { + for(SetsuAvailDevice *adev = setsu->avail_dev; adev;) + { + if(adev->connect_dirty) + { + SetsuEvent event = { 0 }; + event.type = SETSU_EVENT_DEVICE_ADDED; + event.path = adev->path; + cb(&event, user); + adev->connect_dirty = false; + } + if(adev->disconnect_dirty) + { + SetsuEvent event = { 0 }; + event.type = SETSU_EVENT_DEVICE_REMOVED; + event.path = adev->path; + cb(&event, user); + // kill the device only after sending the event + SetsuAvailDevice *next = adev->next; + kill_avail_device(setsu, adev); + adev = next; + continue; + } + adev = adev->next; + } + for(SetsuDevice *dev = setsu->dev; dev; dev = dev->next) poll_device(setsu, dev, cb, user); } @@ -303,7 +384,8 @@ static void device_event(Setsu *setsu, SetsuDevice *dev, struct input_event *ev, S.pos_dirty = false; } S.tracking_id = ev->value; - S.downed = true; + if(ev->value != -1) + S.downed = true; break; case ABS_MT_POSITION_X: S.x = ev->value; @@ -317,6 +399,7 @@ static void device_event(Setsu *setsu, SetsuDevice *dev, struct input_event *ev, } else if(ev->type == EV_SYN && ev->code == SYN_REPORT) device_drain(setsu, dev, cb, user); +#undef S } static void device_drain(Setsu *setsu, SetsuDevice *dev, SetsuEventCb cb, void *user) diff --git a/setsu/test/main.c b/setsu/test/main.c index 6bc5882..f9bdbab 100644 --- a/setsu/test/main.c +++ b/setsu/test/main.c @@ -86,6 +86,11 @@ void event(SetsuEvent *event, void *user) dirty = true; switch(event->type) { + case SETSU_EVENT_DEVICE_ADDED: + setsu_connect(setsu, event->path); + break; + case SETSU_EVENT_DEVICE_REMOVED: + break; case SETSU_EVENT_DOWN: for(size_t i=0; i Date: Wed, 1 Jul 2020 20:48:01 +0200 Subject: [PATCH 012/237] Add udev monitor to setsu --- setsu/include/setsu.h | 1 + setsu/src/setsu.c | 64 ++++++++++++++++++++++++++++++++++++++----- setsu/test/main.c | 37 ++++++++++++++++++++++--- 3 files changed, 91 insertions(+), 11 deletions(-) diff --git a/setsu/include/setsu.h b/setsu/include/setsu.h index eaf32a6..3e8b51e 100644 --- a/setsu/include/setsu.h +++ b/setsu/include/setsu.h @@ -71,5 +71,6 @@ void setsu_free(Setsu *setsu); void setsu_poll(Setsu *setsu, SetsuEventCb cb, void *user); SetsuDevice *setsu_connect(Setsu *setsu, const char *path); void setsu_disconnect(Setsu *setsu, SetsuDevice *dev); +const char *setsu_device_get_path(SetsuDevice *dev); #endif diff --git a/setsu/src/setsu.c b/setsu/src/setsu.c index 56682fb..6a4eb38 100644 --- a/setsu/src/setsu.c +++ b/setsu/src/setsu.c @@ -29,7 +29,13 @@ #include +#define ENABLE_LOG + +#ifdef ENABLE_LOG #define SETSU_LOG(...) fprintf(stderr, __VA_ARGS__) +#else +#define SETSU_LOG(...) do {} while(0) +#endif typedef struct setsu_avail_device_t { @@ -68,12 +74,13 @@ typedef struct setsu_device_t struct setsu_t { struct udev *udev; + struct udev_monitor *udev_mon; SetsuAvailDevice *avail_dev; SetsuDevice *dev; }; static void scan_udev(Setsu *setsu); -static void update_udev_device(Setsu *setsu, struct udev_device *dev, bool added); +static void update_udev_device(Setsu *setsu, struct udev_device *dev); static SetsuDevice *connect(Setsu *setsu, const char *path); static void disconnect(Setsu *setsu, SetsuDevice *dev); static void poll_device(Setsu *setsu, SetsuDevice *dev, SetsuEventCb cb, void *user); @@ -93,7 +100,14 @@ Setsu *setsu_new() return NULL; } - // TODO: monitor + setsu->udev_mon = udev_monitor_new_from_netlink(setsu->udev, "udev"); + if(setsu->udev_mon) + { + udev_monitor_filter_add_match_subsystem_devtype(setsu->udev_mon, "input", NULL); + udev_monitor_enable_receiving(setsu->udev_mon); + } + else + SETSU_LOG("Failed to create udev monitor\n"); scan_udev(setsu); @@ -104,6 +118,8 @@ void setsu_free(Setsu *setsu) { while(setsu->dev) setsu_disconnect(setsu, setsu->dev); + if(setsu->udev_mon) + udev_monitor_unref(setsu->udev_mon); udev_unref(setsu->udev); free(setsu); } @@ -131,8 +147,7 @@ static void scan_udev(Setsu *setsu) struct udev_device *dev = udev_device_new_from_syspath(setsu->udev, path); if(!dev) continue; - SETSU_LOG("enum device: %s\n", path); - update_udev_device(setsu, dev, true); + update_udev_device(setsu, dev); udev_device_unref(dev); } @@ -166,14 +181,23 @@ static bool is_device_interesting(struct udev_device *dev) return false; } -static void update_udev_device(Setsu *setsu, struct udev_device *dev, bool added) +static void update_udev_device(Setsu *setsu, struct udev_device *dev) { if(!is_device_interesting(dev)) return; const char *path = udev_device_get_devnode(dev); if(!path) return; - + + const char *action = udev_device_get_action(dev); + bool added; + if(!action || !strcmp(action, "add")) + added = true; + else if(!strcmp(action, "remove")) + added = false; + else + return; + for(SetsuAvailDevice *adev = setsu->avail_dev; adev; adev=adev->next) { if(!strcmp(adev->path, path)) @@ -182,6 +206,7 @@ static void update_udev_device(Setsu *setsu, struct udev_device *dev, bool added return; // already added, do nothing // disconnected adev->disconnect_dirty = true; + return; } } // not yet added @@ -199,6 +224,20 @@ static void update_udev_device(Setsu *setsu, struct udev_device *dev, bool added adev->connect_dirty = true; } +static void poll_udev_monitor(Setsu *setsu) +{ + if(!setsu->udev_mon) + return; + while(1) + { + struct udev_device *dev = udev_monitor_receive_device(setsu->udev_mon); + if(!dev) + break; + update_udev_device(setsu, dev); + udev_device_unref(dev); + } +} + SetsuDevice *setsu_connect(Setsu *setsu, const char *path) { SetsuDevice *dev = calloc(1, sizeof(SetsuDevice)); @@ -264,6 +303,11 @@ void setsu_disconnect(Setsu *setsu, SetsuDevice *dev) free(dev); } +const char *setsu_device_get_path(SetsuDevice *dev) +{ + return dev->path; +} + void kill_avail_device(Setsu *setsu, SetsuAvailDevice *adev) { for(SetsuDevice *dev = setsu->dev; dev;) @@ -273,6 +317,7 @@ void kill_avail_device(Setsu *setsu, SetsuAvailDevice *adev) SetsuDevice *next = dev->next; setsu_disconnect(setsu, dev); dev = next; + continue; } dev = dev->next; } @@ -296,6 +341,8 @@ void kill_avail_device(Setsu *setsu, SetsuAvailDevice *adev) void setsu_poll(Setsu *setsu, SetsuEventCb cb, void *user) { + poll_udev_monitor(setsu); + for(SetsuAvailDevice *adev = setsu->avail_dev; adev;) { if(adev->connect_dirty) @@ -345,9 +392,12 @@ static void poll_device(Setsu *setsu, SetsuDevice *dev, SetsuEventCb cb, void *u device_event(setsu, dev, &ev, cb, user); else if(r == LIBEVDEV_READ_STATUS_SYNC) sync = true; + else if(r == -ENODEV) { break; } // device probably disconnected, udev remove event should follow soon else { - SETSU_LOG("evdev poll failed\n"); + char buf[256]; + strerror_r(-r, buf, sizeof(buf)); + SETSU_LOG("evdev poll failed: %s\n", buf); break; } } diff --git a/setsu/test/main.c b/setsu/test/main.c index f9bdbab..0244cad 100644 --- a/setsu/test/main.c +++ b/setsu/test/main.c @@ -36,6 +36,9 @@ struct { } touches[TOUCHES_MAX]; bool dirty = false; +bool log_mode; + +#define LOG(...) do { if(log_mode) fprintf(stderr, __VA_ARGS__); } while(0) void quit() { @@ -86,12 +89,16 @@ void event(SetsuEvent *event, void *user) dirty = true; switch(event->type) { - case SETSU_EVENT_DEVICE_ADDED: - setsu_connect(setsu, event->path); + case SETSU_EVENT_DEVICE_ADDED: { + SetsuDevice *dev = setsu_connect(setsu, event->path); + LOG("Device added: %s, connect %s\n", event->path, dev ? "succeeded" : "FAILED!"); break; + } case SETSU_EVENT_DEVICE_REMOVED: + LOG("Device removed: %s\n", event->path); break; case SETSU_EVENT_DOWN: + LOG("Down for %s, tracking id %d\n", setsu_device_get_path(event->dev), event->tracking_id); for(size_t i=0; itype == SETSU_EVENT_UP) + LOG("Up for %s, tracking id %d\n", setsu_device_get_path(event->dev), event->tracking_id); + else + LOG("Position for %s, tracking id %d: %u, %u\n", setsu_device_get_path(event->dev), + event->tracking_id, (unsigned int)event->x, (unsigned int)event->y); for(size_t i=0; itracking_id) @@ -126,8 +138,25 @@ void event(SetsuEvent *event, void *user) } } -int main() +void usage(const char *prog) { + printf("usage: %s [-l]\n -l log mode\n", prog); + exit(1); +} + +int main(int argc, const char *argv[]) +{ + log_mode = false; + if(argc == 2) + { + if(!strcmp(argv[1], "-l")) + log_mode = true; + else + usage(argv[0]); + } + else if(argc != 1) + usage(argv[0]); + memset(touches, 0, sizeof(touches)); setsu = setsu_new(); if(!setsu) @@ -138,7 +167,7 @@ int main() dirty = true; while(1) { - if(dirty) + if(dirty && !log_mode) print_state(); dirty = false; setsu_poll(setsu, event, NULL); From 000a125a8dc1ee7e41c9d2b65817e44b05a254a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Wed, 1 Jul 2020 20:55:26 +0200 Subject: [PATCH 013/237] Add width/height to Setsu --- setsu/include/setsu.h | 2 ++ setsu/src/setsu.c | 14 ++++++++++++-- setsu/test/main.c | 11 +---------- 3 files changed, 15 insertions(+), 12 deletions(-) diff --git a/setsu/include/setsu.h b/setsu/include/setsu.h index 3e8b51e..a34250a 100644 --- a/setsu/include/setsu.h +++ b/setsu/include/setsu.h @@ -72,5 +72,7 @@ void setsu_poll(Setsu *setsu, SetsuEventCb cb, void *user); SetsuDevice *setsu_connect(Setsu *setsu, const char *path); void setsu_disconnect(Setsu *setsu, SetsuDevice *dev); const char *setsu_device_get_path(SetsuDevice *dev); +uint32_t setsu_device_get_width(SetsuDevice *dev); +uint32_t setsu_device_get_height(SetsuDevice *dev); #endif diff --git a/setsu/src/setsu.c b/setsu/src/setsu.c index 6a4eb38..2e78e9a 100644 --- a/setsu/src/setsu.c +++ b/setsu/src/setsu.c @@ -308,6 +308,16 @@ const char *setsu_device_get_path(SetsuDevice *dev) return dev->path; } +uint32_t setsu_device_get_width(SetsuDevice *dev) +{ + return dev->max_x - dev->min_x; +} + +uint32_t setsu_device_get_height(SetsuDevice *dev) +{ + return dev->max_y - dev->min_y; +} + void kill_avail_device(Setsu *setsu, SetsuAvailDevice *adev) { for(SetsuDevice *dev = setsu->dev; dev;) @@ -477,8 +487,8 @@ static void device_drain(Setsu *setsu, SetsuDevice *dev, SetsuEventCb cb, void * { BEGIN_EVENT(SETSU_EVENT_POSITION); event.tracking_id = dev->slots[i].tracking_id; - event.x = dev->slots[i].x; - event.y = dev->slots[i].y; + event.x = (uint32_t)(dev->slots[i].x - dev->min_x); + event.y = (uint32_t)(dev->slots[i].y - dev->min_y); SEND_EVENT(); dev->slots[i].pos_dirty = false; } diff --git a/setsu/test/main.c b/setsu/test/main.c index 0244cad..41401ac 100644 --- a/setsu/test/main.c +++ b/setsu/test/main.c @@ -28,6 +28,7 @@ Setsu *setsu; #define WIDTH 1920 #define HEIGHT 942 #define TOUCHES_MAX 8 +#define SCALE 16 struct { bool down; @@ -47,15 +48,6 @@ void quit() void print_state() { - for(size_t i=0; i Date: Wed, 1 Jul 2020 22:31:35 +0200 Subject: [PATCH 014/237] Build setsu from root (#267) --- CMakeLists.txt | 33 ++++++++++++++++++++++++++++++++- setsu/cmake/FindEvdev.cmake | 19 ++++++++----------- setsu/cmake/FindUdev.cmake | 20 ++++++++------------ 3 files changed, 48 insertions(+), 24 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 459002c..551bb58 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -3,11 +3,18 @@ cmake_minimum_required(VERSION 3.2) project(chiaki) +# Like option(), but the value can also be AUTO +macro(tri_option name desc default) + set("${name}" "${default}" CACHE STRING "${desc}") + set_property(CACHE "${name}" PROPERTY STRINGS AUTO ON OFF) +endmacro() + option(CHIAKI_ENABLE_TESTS "Enable tests for Chiaki" ON) option(CHIAKI_ENABLE_CLI "Enable CLI for Chiaki" OFF) option(CHIAKI_ENABLE_GUI "Enable Qt GUI" ON) option(CHIAKI_ENABLE_ANDROID "Enable Android (Use only as part of the Gradle Project)" OFF) option(CHIAKI_ENABLE_SWITCH "Enable Nintendo Switch (Requires devKitPro libnx)" OFF) +tri_option(CHIAKI_ENABLE_SETSU "Enable libsetsu for touchpad input from controller" AUTO) option(CHIAKI_LIB_ENABLE_OPUS "Use Opus as part of Chiaki Lib" ON) option(CHIAKI_LIB_ENABLE_MBEDTLS "Use mbedtls instead of OpenSSL as part of Chiaki Lib" OFF) option(CHIAKI_LIB_OPENSSL_EXTERNAL_PROJECT "Use OpenSSL as CMake external project" OFF) @@ -30,7 +37,7 @@ set(CPACK_DEBIAN_PACKAGE_DESCRIPTION ${CPACK_PACKAGE_DESCRIPTION}) set(CPACK_DEBIAN_PACKAGE_SECTION "games") include(CPack) -list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake") +list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake;${CMAKE_CURRENT_SOURCE_DIR}/setsu/cmake") # configure nintendo switch toolchain if(CHIAKI_ENABLE_SWITCH AND CHIAKI_ENABLE_SWITCH_LINUX) @@ -64,6 +71,30 @@ if(CHIAKI_ENABLE_CLI) add_subdirectory(cli) endif() +if(CHIAKI_ENABLE_SETSU) + find_package(Udev) + find_package(Evdev) + if(Udev_FOUND AND Evdev_FOUND) + set(CHIAKI_ENABLE_SETSU ON) + else() + if(NOT CHIAKI_ENABLE_SETSU STREQUAL AUTO) + message(FATAL_ERROR " +CHIAKI_ENABLE_SETSU is set to ON, but its dependencies (udev and evdev) could not be resolved. +Keep in mind that setsu is only supported on Linux!") + endif() + set(CHIAKI_ENABLE_SETSU OFF) + endif() + if(CHIAKI_ENABLE_SETSU) + add_subdirectory(setsu) + endif() +endif() + +if(CHIAKI_ENABLE_SETSU) + message(STATUS "Setsu enabled") +else() + message(STATUS "Setsu disabled") +endif() + if(CHIAKI_ENABLE_GUI) #add_subdirectory(setsu) add_subdirectory(gui) diff --git a/setsu/cmake/FindEvdev.cmake b/setsu/cmake/FindEvdev.cmake index a6be138..0703889 100644 --- a/setsu/cmake/FindEvdev.cmake +++ b/setsu/cmake/FindEvdev.cmake @@ -3,18 +3,15 @@ set(_prefix Evdev) set(_target "${_prefix}::libevdev") -find_package(PkgConfig REQUIRED) +find_package(PkgConfig) -pkg_check_modules("${_prefix}" libevdev) - -function(resolve_location) - -endfunction() - -if(${_prefix}_FOUND) - add_library("${_target}" INTERFACE IMPORTED) - target_link_libraries("${_target}" INTERFACE ${${_prefix}_LIBRARIES}) - target_include_directories("${_target}" INTERFACE ${${_prefix}_INCLUDE_DIRS}) +if(PkgConfig_FOUND) + pkg_check_modules("${_prefix}" libevdev) + if(${_prefix}_FOUND AND NOT TARGET "${_target}") + add_library("${_target}" INTERFACE IMPORTED) + target_link_libraries("${_target}" INTERFACE ${${_prefix}_LIBRARIES}) + target_include_directories("${_target}" INTERFACE ${${_prefix}_INCLUDE_DIRS}) + endif() endif() include(FindPackageHandleStandardArgs) diff --git a/setsu/cmake/FindUdev.cmake b/setsu/cmake/FindUdev.cmake index 369f806..a35b5f6 100644 --- a/setsu/cmake/FindUdev.cmake +++ b/setsu/cmake/FindUdev.cmake @@ -3,18 +3,14 @@ set(_prefix Udev) set(_target "${_prefix}::libudev") -find_package(PkgConfig REQUIRED) - -pkg_check_modules("${_prefix}" libudev) - -function(resolve_location) - -endfunction() - -if(${_prefix}_FOUND) - add_library("${_target}" INTERFACE IMPORTED) - target_link_libraries("${_target}" INTERFACE ${${_prefix}_LIBRARIES}) - target_include_directories("${_target}" INTERFACE ${${_prefix}_INCLUDE_DIRS}) +find_package(PkgConfig) +if(PkgConfig_FOUND) + pkg_check_modules("${_prefix}" libudev) + if(${_prefix}_FOUND AND NOT TARGET "${_target}") + add_library("${_target}" INTERFACE IMPORTED) + target_link_libraries("${_target}" INTERFACE ${${_prefix}_LIBRARIES}) + target_include_directories("${_target}" INTERFACE ${${_prefix}_INCLUDE_DIRS}) + endif() endif() include(FindPackageHandleStandardArgs) From 0b9f8798eb3b6597b446ecb1c1a50ffeee9c3081 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Thu, 2 Jul 2020 19:16:23 +0200 Subject: [PATCH 015/237] Add Touches to Controller State --- lib/include/chiaki/controller.h | 25 +++++++++++++++++++++++-- lib/src/controller.c | 13 +++++++++++++ 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/lib/include/chiaki/controller.h b/lib/include/chiaki/controller.h index 0239c6c..2a870d0 100644 --- a/lib/include/chiaki/controller.h +++ b/lib/include/chiaki/controller.h @@ -56,6 +56,14 @@ typedef enum chiaki_controller_analog_button_t CHIAKI_CONTROLLER_ANALOG_BUTTON_R2 = (1 << 17) } ChiakiControllerAnalogButton; +typedef struct chiaki_controller_touch_t +{ + uint16_t x, y; + int8_t id; // -1 = up +} ChiakiControllerTouch; + +#define CHIAKI_CONTROLLER_TOUCHES_MAX 2 + typedef struct chiaki_controller_state_t { /** @@ -70,19 +78,32 @@ typedef struct chiaki_controller_state_t int16_t left_y; int16_t right_x; int16_t right_y; + + uint8_t touch_id_next; + ChiakiControllerTouch touches[CHIAKI_CONTROLLER_TOUCHES_MAX]; } ChiakiControllerState; CHIAKI_EXPORT void chiaki_controller_state_set_idle(ChiakiControllerState *state); static inline bool chiaki_controller_state_equals(ChiakiControllerState *a, ChiakiControllerState *b) { - return a->buttons == b->buttons + if(!(a->buttons == b->buttons && a->l2_state == b->l2_state && a->r2_state == b->r2_state && a->left_x == b->left_x && a->left_y == b->left_y && a->right_x == b->right_x - && a->right_y == b->right_y; + && a->right_y == b->right_y)) + return false; + + for(size_t i=0; itouches[i].id != b->touches[i].id) + return false; + if(a->touches[i].id >= 0 && (a->touches[i].x != b->touches[i].x || a->touches[i].y != b->touches[i].y)) + return false; + } + return true; } CHIAKI_EXPORT void chiaki_controller_state_or(ChiakiControllerState *out, ChiakiControllerState *a, ChiakiControllerState *b); diff --git a/lib/src/controller.c b/lib/src/controller.c index f71371c..a3853bd 100644 --- a/lib/src/controller.c +++ b/lib/src/controller.c @@ -39,4 +39,17 @@ CHIAKI_EXPORT void chiaki_controller_state_or(ChiakiControllerState *out, Chiaki out->left_y = MAX_ABS(a->left_y, b->left_y); out->right_x = MAX_ABS(a->right_x, b->right_x); out->right_y = MAX_ABS(a->right_y, b->right_y); + + out->touch_id_next = 0; + for(size_t i=0; itouches[i].id >= 0 ? &a->touches[i] : b->touches[i].id >= 0 ? &b->touches[i] : NULL; + if(!touch) + { + out->touches[i].id = -1; + out->touches[i].x = out->touches[i].y = 0; + continue; + } + out->touches[i] = *touch; + } } From 02d026f20528a30da53ef61fbbb94d0802a0c9a7 Mon Sep 17 00:00:00 2001 From: Florian Maerkl Date: Thu, 2 Jul 2020 13:04:02 +0200 Subject: [PATCH 016/237] Add Touchpad Feedback History Formatting --- lib/include/chiaki/feedback.h | 10 +++++++++- lib/src/feedback.c | 11 +++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/lib/include/chiaki/feedback.h b/lib/include/chiaki/feedback.h index 6a74ae1..19e9d7f 100644 --- a/lib/include/chiaki/feedback.h +++ b/lib/include/chiaki/feedback.h @@ -44,7 +44,7 @@ typedef struct chiaki_feedback_state_t CHIAKI_EXPORT void chiaki_feedback_state_format(uint8_t *buf, ChiakiFeedbackState *state); -#define CHIAKI_HISTORY_EVENT_SIZE_MAX 0x3 // TODO: will be bigger later for touchpad at least +#define CHIAKI_HISTORY_EVENT_SIZE_MAX 0x5 typedef struct chiaki_feedback_history_event_t { @@ -58,6 +58,14 @@ typedef struct chiaki_feedback_history_event_t */ CHIAKI_EXPORT ChiakiErrorCode chiaki_feedback_history_event_set_button(ChiakiFeedbackHistoryEvent *event, uint64_t button, uint8_t state); +/** + * @param pointer_id identifier for the touch from 0 to 127 + * @param x from 0 to 1920 + * @param y from 0 to 942 + */ +CHIAKI_EXPORT ChiakiErrorCode chiaki_feedback_history_event_set_touchpad(ChiakiFeedbackHistoryEvent *event, + bool down, uint8_t pointer_id, uint16_t x, uint16_t y); + /** * Ring buffer of ChiakiFeedbackHistoryEvent */ diff --git a/lib/src/feedback.c b/lib/src/feedback.c index c294264..b852f7d 100644 --- a/lib/src/feedback.c +++ b/lib/src/feedback.c @@ -119,6 +119,17 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_feedback_history_event_set_button(ChiakiFee return CHIAKI_ERR_SUCCESS; } +CHIAKI_EXPORT ChiakiErrorCode chiaki_feedback_history_event_set_touchpad(ChiakiFeedbackHistoryEvent *event, + bool down, uint8_t pointer_id, uint16_t x, uint16_t y) +{ + event->len = 5; + event->buf[0] = down ? 0xd0 : 0xc0; + event->buf[1] = pointer_id / 0x7f; + event->buf[2] = (uint8_t)(x >> 4); + event->buf[3] = (uint8_t)((x & 0xf) << 4) | (uint8_t)(y >> 8); + event->buf[4] = (uint8_t)y; +} + CHIAKI_EXPORT ChiakiErrorCode chiaki_feedback_history_buffer_init(ChiakiFeedbackHistoryBuffer *feedback_history_buffer, size_t size) { feedback_history_buffer->events = calloc(size, sizeof(ChiakiFeedbackHistoryEvent)); From cd3d700df832db0a509f98115de8c2357ef2c372 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Thu, 2 Jul 2020 19:40:34 +0200 Subject: [PATCH 017/237] Add Touchpad to Feedback Sender --- lib/include/chiaki/feedback.h | 2 +- lib/src/feedback.c | 2 +- lib/src/feedbacksender.c | 37 +++++++++++++++++++++++++++++++++-- 3 files changed, 37 insertions(+), 4 deletions(-) diff --git a/lib/include/chiaki/feedback.h b/lib/include/chiaki/feedback.h index 19e9d7f..83cb08a 100644 --- a/lib/include/chiaki/feedback.h +++ b/lib/include/chiaki/feedback.h @@ -63,7 +63,7 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_feedback_history_event_set_button(ChiakiFee * @param x from 0 to 1920 * @param y from 0 to 942 */ -CHIAKI_EXPORT ChiakiErrorCode chiaki_feedback_history_event_set_touchpad(ChiakiFeedbackHistoryEvent *event, +CHIAKI_EXPORT void chiaki_feedback_history_event_set_touchpad(ChiakiFeedbackHistoryEvent *event, bool down, uint8_t pointer_id, uint16_t x, uint16_t y); /** diff --git a/lib/src/feedback.c b/lib/src/feedback.c index b852f7d..d7a8b49 100644 --- a/lib/src/feedback.c +++ b/lib/src/feedback.c @@ -119,7 +119,7 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_feedback_history_event_set_button(ChiakiFee return CHIAKI_ERR_SUCCESS; } -CHIAKI_EXPORT ChiakiErrorCode chiaki_feedback_history_event_set_touchpad(ChiakiFeedbackHistoryEvent *event, +CHIAKI_EXPORT void chiaki_feedback_history_event_set_touchpad(ChiakiFeedbackHistoryEvent *event, bool down, uint8_t pointer_id, uint16_t x, uint16_t y) { event->len = 5; diff --git a/lib/src/feedbacksender.c b/lib/src/feedbacksender.c index b812d39..2fee4b6 100644 --- a/lib/src/feedbacksender.c +++ b/lib/src/feedbacksender.c @@ -118,9 +118,19 @@ static void feedback_sender_send_state(ChiakiFeedbackSender *feedback_sender) static bool controller_state_equals_for_feedback_history(ChiakiControllerState *a, ChiakiControllerState *b) { - return a->buttons == b->buttons + if(!(a->buttons == b->buttons && a->l2_state == b->l2_state - && a->r2_state == b->r2_state; + && a->r2_state == b->r2_state)) + return false; + + for(size_t i=0; itouches[i].id != b->touches[i].id) + return false; + if(a->touches[i].id >= 0 && (a->touches[i].x != b->touches[i].x || a->touches[i].y != b->touches[i].y)) + return false; + } + return true; } static void feedback_sender_send_history_packet(ChiakiFeedbackSender *feedback_sender) @@ -189,6 +199,29 @@ static void feedback_sender_send_history(ChiakiFeedbackSender *feedback_sender) else CHIAKI_LOGE(feedback_sender->log, "Feedback Sender failed to format button history event for R2"); } + + for(size_t i=0; itouches[i].id != state_now->touches[i].id && state_prev->touches[i].id >= 0) + { + ChiakiFeedbackHistoryEvent event; + chiaki_feedback_history_event_set_touchpad(&event, false, (uint8_t)state_prev->touches[i].id, + state_prev->touches[i].x, state_prev->touches[i].y); + chiaki_feedback_history_buffer_push(&feedback_sender->history_buf, &event); + feedback_sender_send_history_packet(feedback_sender); + } + else if(state_now->touches[i].id >= 0 + && (state_prev->touches[i].id != state_now->touches[i].id + || state_prev->touches[i].x != state_now->touches[i].x + || state_prev->touches[i].y != state_now->touches[i].y)) + { + ChiakiFeedbackHistoryEvent event; + chiaki_feedback_history_event_set_touchpad(&event, true, (uint8_t)state_now->touches[i].id, + state_now->touches[i].x, state_now->touches[i].y); + chiaki_feedback_history_buffer_push(&feedback_sender->history_buf, &event); + feedback_sender_send_history_packet(feedback_sender); + } + } } static bool state_cond_check(void *user) From 7d4cbccfbf71580b971cf1d2dc4f73e2b14c500f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Thu, 2 Jul 2020 19:57:36 +0200 Subject: [PATCH 018/237] Add Touch Tracking Functions to Controller State --- lib/include/chiaki/controller.h | 9 +++++++ lib/src/controller.c | 44 +++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+) diff --git a/lib/include/chiaki/controller.h b/lib/include/chiaki/controller.h index 2a870d0..258cd89 100644 --- a/lib/include/chiaki/controller.h +++ b/lib/include/chiaki/controller.h @@ -85,6 +85,15 @@ typedef struct chiaki_controller_state_t CHIAKI_EXPORT void chiaki_controller_state_set_idle(ChiakiControllerState *state); +/** + * @return A non-negative newly allocated touch id allocated or -1 if there are no slots left + */ +CHIAKI_EXPORT int8_t chiaki_controller_state_start_touch(ChiakiControllerState *state, uint16_t x, uint16_t y); + +CHIAKI_EXPORT void chiaki_controller_state_stop_touch(ChiakiControllerState *state, uint8_t id); + +CHIAKI_EXPORT void chiaki_controller_state_set_touch_pos(ChiakiControllerState *state, uint8_t id, uint16_t x, uint16_t y); + static inline bool chiaki_controller_state_equals(ChiakiControllerState *a, ChiakiControllerState *b) { if(!(a->buttons == b->buttons diff --git a/lib/src/controller.c b/lib/src/controller.c index a3853bd..a50cac4 100644 --- a/lib/src/controller.c +++ b/lib/src/controller.c @@ -17,6 +17,8 @@ #include +#define TOUCH_ID_MASK 0x7f + CHIAKI_EXPORT void chiaki_controller_state_set_idle(ChiakiControllerState *state) { state->buttons = 0; @@ -26,6 +28,48 @@ CHIAKI_EXPORT void chiaki_controller_state_set_idle(ChiakiControllerState *state state->right_y = 0; } +CHIAKI_EXPORT int8_t chiaki_controller_state_start_touch(ChiakiControllerState *state, uint16_t x, uint16_t y) +{ + for(size_t i=0; itouches[i].id < 0) + { + state->touches[i].id = state->touch_id_next; + state->touch_id_next = (state->touch_id_next + 1) & TOUCH_ID_MASK; + state->touches[i].x = x; + state->touches[i].y = y; + break; + } + } + return -1; +} + +CHIAKI_EXPORT void chiaki_controller_state_stop_touch(ChiakiControllerState *state, uint8_t id) +{ + for(size_t i=0; itouches[i].id == id) + { + state->touches[i].id = -1; + break; + } + } +} + +CHIAKI_EXPORT void chiaki_controller_state_set_touch_pos(ChiakiControllerState *state, uint8_t id, uint16_t x, uint16_t y) +{ + id &= TOUCH_ID_MASK; + for(size_t i=0; itouches[i].id == id) + { + state->touches[i].x = x; + state->touches[i].y = y; + break; + } + } +} + #define MAX(a, b) ((a) > (b) ? (a) : (b)) #define ABS(a) ((a) > 0 ? (a) : -(a)) #define MAX_ABS(a, b) (ABS(a) > ABS(b) ? (a) : (b)) From 699d6bb1d9c29ae91a0608c39aab7d402d65585d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Thu, 2 Jul 2020 21:18:03 +0200 Subject: [PATCH 019/237] Fix Sourcehut OpenBSD Image (#268) --- .builds/openbsd.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.builds/openbsd.yml b/.builds/openbsd.yml index b384168..b7f5156 100644 --- a/.builds/openbsd.yml +++ b/.builds/openbsd.yml @@ -1,5 +1,5 @@ -image: openbsd/latest +image: openbsd/6.7 sources: - https://github.com/thestr4ng3r/chiaki From 57b0b683e80aee9a32cd4e1a89b3faaa313f348e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Thu, 2 Jul 2020 21:18:12 +0200 Subject: [PATCH 020/237] Drop Support for QtGamepad --- CMakeLists.txt | 1 - gui/CMakeLists.txt | 10 ----- gui/include/streamsession.h | 10 ----- gui/src/streamsession.cpp | 81 ------------------------------------- scripts/appveyor.sh | 1 - scripts/travis-build.sh | 1 - 6 files changed, 104 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 551bb58..9e3f063 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -18,7 +18,6 @@ tri_option(CHIAKI_ENABLE_SETSU "Enable libsetsu for touchpad input from controll option(CHIAKI_LIB_ENABLE_OPUS "Use Opus as part of Chiaki Lib" ON) option(CHIAKI_LIB_ENABLE_MBEDTLS "Use mbedtls instead of OpenSSL as part of Chiaki Lib" OFF) option(CHIAKI_LIB_OPENSSL_EXTERNAL_PROJECT "Use OpenSSL as CMake external project" OFF) -option(CHIAKI_GUI_ENABLE_QT_GAMEPAD "Use QtGamepad for Input" OFF) option(CHIAKI_GUI_ENABLE_SDL_GAMECONTROLLER "Use SDL Gamecontroller for Input" ON) option(CHIAKI_CLI_ARGP_STANDALONE "Search for standalone argp lib for CLI" OFF) diff --git a/gui/CMakeLists.txt b/gui/CMakeLists.txt index 072320f..210e513 100644 --- a/gui/CMakeLists.txt +++ b/gui/CMakeLists.txt @@ -6,12 +6,6 @@ find_package(Qt5 REQUIRED COMPONENTS Core Widgets Gui Multimedia OpenGL Svg) if(APPLE) find_package(Qt5 REQUIRED COMPONENTS MacExtras) endif() -if(CHIAKI_GUI_ENABLE_QT_GAMEPAD AND CHIAKI_GUI_ENABLE_SDL_GAMECONTROLLER) - message(FATAL_ERROR "Only one input method may be enabled. Disable either CHIAKI_GUI_ENABLE_SDL_GAMECONTROLLER or CHIAKI_GUI_ENABLE_QT_GAMEPAD.") -endif() -if(CHIAKI_GUI_ENABLE_QT_GAMEPAD) - find_package(Qt5 REQUIRED COMPONENTS Gamepad) -endif() if(CHIAKI_GUI_ENABLE_SDL_GAMECONTROLLER) find_package(SDL2 MODULE REQUIRED) endif() @@ -85,10 +79,6 @@ if(APPLE) target_link_libraries(chiaki Qt5::MacExtras) target_compile_definitions(chiaki PRIVATE CHIAKI_GUI_ENABLE_QT_MACEXTRAS) endif() -if(CHIAKI_GUI_ENABLE_QT_GAMEPAD) - target_link_libraries(chiaki Qt5::Gamepad) - target_compile_definitions(chiaki PRIVATE CHIAKI_GUI_ENABLE_QT_GAMEPAD) -endif() if(CHIAKI_GUI_ENABLE_SDL_GAMECONTROLLER) target_link_libraries(chiaki SDL2::SDL2) target_compile_definitions(chiaki PRIVATE CHIAKI_GUI_ENABLE_SDL_GAMECONTROLLER) diff --git a/gui/include/streamsession.h b/gui/include/streamsession.h index 15ac4c5..b6a5b29 100644 --- a/gui/include/streamsession.h +++ b/gui/include/streamsession.h @@ -31,10 +31,6 @@ #include #include -#if CHIAKI_GUI_ENABLE_QT_GAMEPAD -class QGamepad; -#endif - class QAudioOutput; class QIODevice; class QKeyEvent; @@ -72,9 +68,6 @@ class StreamSession : public QObject ChiakiSession session; ChiakiOpusDecoder opus_decoder; -#if CHIAKI_GUI_ENABLE_QT_GAMEPAD - QGamepad *gamepad; -#endif Controller *controller; ChiakiControllerState keyboard_state; @@ -103,9 +96,6 @@ class StreamSession : public QObject void SetLoginPIN(const QString &pin); -#if CHIAKI_GUI_ENABLE_QT_GAMEPAD - QGamepad *GetGamepad() { return gamepad; } -#endif Controller *GetController() { return controller; } VideoDecoder *GetVideoDecoder() { return &video_decoder; } diff --git a/gui/src/streamsession.cpp b/gui/src/streamsession.cpp index bad6305..a0c85a1 100644 --- a/gui/src/streamsession.cpp +++ b/gui/src/streamsession.cpp @@ -21,11 +21,6 @@ #include -#if CHIAKI_GUI_ENABLE_QT_GAMEPAD -#include -#include -#endif - #include #include @@ -53,9 +48,6 @@ static void EventCb(ChiakiEvent *event, void *user); StreamSession::StreamSession(const StreamSessionConnectInfo &connect_info, QObject *parent) : QObject(parent), log(this, connect_info.log_level_mask, connect_info.log_file), -#if CHIAKI_GUI_ENABLE_QT_GAMEPAD - gamepad(nullptr), -#endif controller(nullptr), video_decoder(connect_info.hw_decode_engine, log.GetChiakiLog()), audio_output(nullptr), @@ -92,9 +84,6 @@ StreamSession::StreamSession(const StreamSessionConnectInfo &connect_info, QObje chiaki_session_set_video_sample_cb(&session, VideoSampleCb, this); chiaki_session_set_event_cb(&session, EventCb, this); -#if CHIAKI_GUI_ENABLE_QT_GAMEPAD - connect(QGamepadManager::instance(), &QGamepadManager::connectedGamepadsChanged, this, &StreamSession::UpdateGamepads); -#endif #if CHIAKI_GUI_ENABLE_SDL_GAMECONTROLLER connect(ControllerManager::GetInstance(), &ControllerManager::AvailableControllersUpdated, this, &StreamSession::UpdateGamepads); #endif @@ -108,9 +97,6 @@ StreamSession::~StreamSession() chiaki_session_join(&session); chiaki_session_fini(&session); chiaki_opus_decoder_fini(&opus_decoder); -#if CHIAKI_GUI_ENABLE_QT_GAMEPAD - delete gamepad; -#endif #if CHIAKI_GUI_ENABLE_SDL_GAMECONTROLLER delete controller; #endif @@ -202,46 +188,6 @@ void StreamSession::HandleKeyboardEvent(QKeyEvent *event) void StreamSession::UpdateGamepads() { -#if CHIAKI_GUI_ENABLE_QT_GAMEPAD - if(!gamepad || !gamepad->isConnected()) - { - if(gamepad) - { - CHIAKI_LOGI(log.GetChiakiLog(), "Gamepad %d disconnected", gamepad->deviceId()); - delete gamepad; - gamepad = nullptr; - } - const auto connected_pads = QGamepadManager::instance()->connectedGamepads(); - if(!connected_pads.isEmpty()) - { - gamepad = new QGamepad(connected_pads[0], this); - CHIAKI_LOGI(log.GetChiakiLog(), "Gamepad %d connected: \"%s\"", connected_pads[0], gamepad->name().toLocal8Bit().constData()); - connect(gamepad, &QGamepad::buttonAChanged, this, &StreamSession::SendFeedbackState); - connect(gamepad, &QGamepad::buttonBChanged, this, &StreamSession::SendFeedbackState); - connect(gamepad, &QGamepad::buttonXChanged, this, &StreamSession::SendFeedbackState); - connect(gamepad, &QGamepad::buttonYChanged, this, &StreamSession::SendFeedbackState); - connect(gamepad, &QGamepad::buttonLeftChanged, this, &StreamSession::SendFeedbackState); - connect(gamepad, &QGamepad::buttonRightChanged, this, &StreamSession::SendFeedbackState); - connect(gamepad, &QGamepad::buttonUpChanged, this, &StreamSession::SendFeedbackState); - connect(gamepad, &QGamepad::buttonDownChanged, this, &StreamSession::SendFeedbackState); - connect(gamepad, &QGamepad::buttonL1Changed, this, &StreamSession::SendFeedbackState); - connect(gamepad, &QGamepad::buttonR1Changed, this, &StreamSession::SendFeedbackState); - connect(gamepad, &QGamepad::buttonL1Changed, this, &StreamSession::SendFeedbackState); - connect(gamepad, &QGamepad::buttonL2Changed, this, &StreamSession::SendFeedbackState); - connect(gamepad, &QGamepad::buttonL3Changed, this, &StreamSession::SendFeedbackState); - connect(gamepad, &QGamepad::buttonR3Changed, this, &StreamSession::SendFeedbackState); - connect(gamepad, &QGamepad::buttonStartChanged, this, &StreamSession::SendFeedbackState); - connect(gamepad, &QGamepad::buttonSelectChanged, this, &StreamSession::SendFeedbackState); - connect(gamepad, &QGamepad::buttonGuideChanged, this, &StreamSession::SendFeedbackState); - connect(gamepad, &QGamepad::axisLeftXChanged, this, &StreamSession::SendFeedbackState); - connect(gamepad, &QGamepad::axisLeftYChanged, this, &StreamSession::SendFeedbackState); - connect(gamepad, &QGamepad::axisRightXChanged, this, &StreamSession::SendFeedbackState); - connect(gamepad, &QGamepad::axisRightYChanged, this, &StreamSession::SendFeedbackState); - } - } - - SendFeedbackState(); -#endif #if CHIAKI_GUI_ENABLE_SDL_GAMECONTROLLER if(!controller || !controller->IsConnected()) { @@ -273,33 +219,6 @@ void StreamSession::SendFeedbackState() { ChiakiControllerState state = {}; -#if CHIAKI_GUI_ENABLE_QT_GAMEPAD - if(gamepad) - { - state.buttons |= gamepad->buttonA() ? CHIAKI_CONTROLLER_BUTTON_CROSS : 0; - state.buttons |= gamepad->buttonB() ? CHIAKI_CONTROLLER_BUTTON_MOON : 0; - state.buttons |= gamepad->buttonX() ? CHIAKI_CONTROLLER_BUTTON_BOX : 0; - state.buttons |= gamepad->buttonY() ? CHIAKI_CONTROLLER_BUTTON_PYRAMID : 0; - state.buttons |= gamepad->buttonLeft() ? CHIAKI_CONTROLLER_BUTTON_DPAD_LEFT : 0; - state.buttons |= gamepad->buttonRight() ? CHIAKI_CONTROLLER_BUTTON_DPAD_RIGHT : 0; - state.buttons |= gamepad->buttonUp() ? CHIAKI_CONTROLLER_BUTTON_DPAD_UP : 0; - state.buttons |= gamepad->buttonDown() ? CHIAKI_CONTROLLER_BUTTON_DPAD_DOWN : 0; - state.buttons |= gamepad->buttonL1() ? CHIAKI_CONTROLLER_BUTTON_L1 : 0; - state.buttons |= gamepad->buttonR1() ? CHIAKI_CONTROLLER_BUTTON_R1 : 0; - state.buttons |= gamepad->buttonL3() ? CHIAKI_CONTROLLER_BUTTON_L3 : 0; - state.buttons |= gamepad->buttonR3() ? CHIAKI_CONTROLLER_BUTTON_R3 : 0; - state.buttons |= gamepad->buttonStart() ? CHIAKI_CONTROLLER_BUTTON_OPTIONS : 0; - state.buttons |= gamepad->buttonSelect() ? CHIAKI_CONTROLLER_BUTTON_SHARE : 0; - state.buttons |= gamepad->buttonGuide() ? CHIAKI_CONTROLLER_BUTTON_PS : 0; - state.l2_state = (uint8_t)(gamepad->buttonL2() * 0xff); - state.r2_state = (uint8_t)(gamepad->buttonR2() * 0xff); - state.left_x = static_cast(gamepad->axisLeftX() * 0x7fff); - state.left_y = static_cast(gamepad->axisLeftY() * 0x7fff); - state.right_x = static_cast(gamepad->axisRightX() * 0x7fff); - state.right_y = static_cast(gamepad->axisRightY() * 0x7fff); - } -#endif - #if CHIAKI_GUI_ENABLE_SDL_GAMECONTROLLER if(controller) state = controller->GetState(); diff --git a/scripts/appveyor.sh b/scripts/appveyor.sh index 077884a..0e49286 100755 --- a/scripts/appveyor.sh +++ b/scripts/appveyor.sh @@ -58,7 +58,6 @@ cmake \ -DPYTHON_EXECUTABLE="$PYTHON" \ -DCHIAKI_ENABLE_TESTS=ON \ -DCHIAKI_ENABLE_CLI=OFF \ - -DCHIAKI_GUI_ENABLE_QT_GAMEPAD=OFF \ -DCHIAKI_GUI_ENABLE_SDL_GAMECONTROLLER=ON \ .. || exit 1 diff --git a/scripts/travis-build.sh b/scripts/travis-build.sh index e1790dd..a16ec77 100755 --- a/scripts/travis-build.sh +++ b/scripts/travis-build.sh @@ -6,7 +6,6 @@ cmake \ -DCMAKE_PREFIX_PATH=$CMAKE_PREFIX_PATH \ -DCHIAKI_ENABLE_TESTS=ON \ -DCHIAKI_ENABLE_CLI=OFF \ - -DCHIAKI_GUI_ENABLE_QT_GAMEPAD=OFF \ -DCHIAKI_GUI_ENABLE_SDL_GAMECONTROLLER=ON \ $CMAKE_EXTRA_ARGS \ .. || exit 1 From a688974c898a6412017e319089ddf1d408dfccc1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Thu, 2 Jul 2020 21:37:52 +0200 Subject: [PATCH 021/237] Minor Setsu Fixes --- setsu/include/setsu.h | 8 ++++++++ setsu/src/setsu.c | 9 +++++++++ setsu/test/main.c | 17 +++++++++++++---- 3 files changed, 30 insertions(+), 4 deletions(-) diff --git a/setsu/include/setsu.h b/setsu/include/setsu.h index a34250a..0303024 100644 --- a/setsu/include/setsu.h +++ b/setsu/include/setsu.h @@ -20,6 +20,10 @@ #include +#ifdef __cplusplus +extern "C" { +#endif + typedef struct setsu_t Setsu; typedef struct setsu_device_t SetsuDevice; typedef int SetsuTrackingId; @@ -75,4 +79,8 @@ const char *setsu_device_get_path(SetsuDevice *dev); uint32_t setsu_device_get_width(SetsuDevice *dev); uint32_t setsu_device_get_height(SetsuDevice *dev); +#ifdef __cplusplus +} +#endif + #endif diff --git a/setsu/src/setsu.c b/setsu/src/setsu.c index 2e78e9a..bfce543 100644 --- a/setsu/src/setsu.c +++ b/setsu/src/setsu.c @@ -116,11 +116,20 @@ Setsu *setsu_new() void setsu_free(Setsu *setsu) { + if(!setsu) + return; while(setsu->dev) setsu_disconnect(setsu, setsu->dev); if(setsu->udev_mon) udev_monitor_unref(setsu->udev_mon); udev_unref(setsu->udev); + while(setsu->avail_dev) + { + SetsuAvailDevice *adev = setsu->avail_dev; + setsu->avail_dev = adev->next; + free(adev->path); + free(adev); + } free(setsu); } diff --git a/setsu/test/main.c b/setsu/test/main.c index 41401ac..be36535 100644 --- a/setsu/test/main.c +++ b/setsu/test/main.c @@ -22,6 +22,7 @@ #include #include #include +#include Setsu *setsu; @@ -38,12 +39,13 @@ struct { bool dirty = false; bool log_mode; +volatile bool should_quit; #define LOG(...) do { if(log_mode) fprintf(stderr, __VA_ARGS__); } while(0) -void quit() +void sigint(int s) { - setsu_free(setsu); + should_quit = true; } void print_state() @@ -155,15 +157,22 @@ int main(int argc, const char *argv[]) printf("Failed to init setsu\n"); return 1; } + + struct sigaction sa = {0}; + sa.sa_handler = sigint; + sigemptyset(&sa.sa_mask); + sigaction(SIGINT, &sa, NULL); + dirty = true; - while(1) + while(!should_quit) { if(dirty && !log_mode) print_state(); dirty = false; setsu_poll(setsu, event, NULL); } - atexit(quit); + setsu_free(setsu); + printf("\nさよなら!\n"); return 0; } From f3be4ed22a5849fd6951a51945f24df677d3a9ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Thu, 2 Jul 2020 22:34:57 +0200 Subject: [PATCH 022/237] Integrate Setsu in GUI --- gui/CMakeLists.txt | 4 ++ gui/include/streamsession.h | 12 +++++ gui/src/controllermanager.cpp | 3 +- gui/src/streamsession.cpp | 95 ++++++++++++++++++++++++++++++++++- lib/src/controller.c | 16 ++++-- 5 files changed, 124 insertions(+), 6 deletions(-) diff --git a/gui/CMakeLists.txt b/gui/CMakeLists.txt index 210e513..7072b83 100644 --- a/gui/CMakeLists.txt +++ b/gui/CMakeLists.txt @@ -83,6 +83,10 @@ if(CHIAKI_GUI_ENABLE_SDL_GAMECONTROLLER) target_link_libraries(chiaki SDL2::SDL2) target_compile_definitions(chiaki PRIVATE CHIAKI_GUI_ENABLE_SDL_GAMECONTROLLER) endif() +if(CHIAKI_ENABLE_SETSU) + target_link_libraries(chiaki setsu) + target_compile_definitions(chiaki PRIVATE CHIAKI_GUI_ENABLE_SETSU) +endif() set_target_properties(chiaki PROPERTIES MACOSX_BUNDLE TRUE diff --git a/gui/include/streamsession.h b/gui/include/streamsession.h index b6a5b29..94399ad 100644 --- a/gui/include/streamsession.h +++ b/gui/include/streamsession.h @@ -21,6 +21,10 @@ #include #include +#if CHIAKI_GUI_ENABLE_SETSU +#include +#endif + #include "videodecoder.h" #include "exception.h" #include "sessionlog.h" @@ -69,6 +73,11 @@ class StreamSession : public QObject ChiakiOpusDecoder opus_decoder; Controller *controller; +#if CHIAKI_GUI_ENABLE_SETSU + Setsu *setsu; + QMap, uint8_t> setsu_ids; + ChiakiControllerState setsu_state; +#endif ChiakiControllerState keyboard_state; @@ -83,6 +92,9 @@ class StreamSession : public QObject void PushAudioFrame(int16_t *buf, size_t samples_count); void PushVideoSample(uint8_t *buf, size_t buf_size); void Event(ChiakiEvent *event); +#if CHIAKI_GUI_ENABLE_SETSU + void HandleSetsuEvent(SetsuEvent *event); +#endif private slots: void InitAudio(unsigned int channels, unsigned int rate); diff --git a/gui/src/controllermanager.cpp b/gui/src/controllermanager.cpp index 224f555..2a52ea6 100644 --- a/gui/src/controllermanager.cpp +++ b/gui/src/controllermanager.cpp @@ -199,7 +199,8 @@ QString Controller::GetName() ChiakiControllerState Controller::GetState() { - ChiakiControllerState state = {}; + ChiakiControllerState state; + chiaki_controller_state_set_idle(&state); #ifdef CHIAKI_GUI_ENABLE_SDL_GAMECONTROLLER if(!controller) return state; diff --git a/gui/src/streamsession.cpp b/gui/src/streamsession.cpp index a0c85a1..dd4104a 100644 --- a/gui/src/streamsession.cpp +++ b/gui/src/streamsession.cpp @@ -27,6 +27,8 @@ #include #include +#define SETSU_UPDATE_INTERVAL_MS 4 + StreamSessionConnectInfo::StreamSessionConnectInfo(Settings *settings, QString host, QByteArray regist_key, QByteArray morning) { key_map = settings->GetControllerMappingForDecoding(); @@ -44,6 +46,7 @@ static void AudioSettingsCb(uint32_t channels, uint32_t rate, void *user); static void AudioFrameCb(int16_t *buf, size_t samples_count, void *user); static bool VideoSampleCb(uint8_t *buf, size_t buf_size, void *user); static void EventCb(ChiakiEvent *event, void *user); +static void SessionSetsuCb(SetsuEvent *event, void *user); StreamSession::StreamSession(const StreamSessionConnectInfo &connect_info, QObject *parent) : QObject(parent), @@ -70,7 +73,7 @@ StreamSession::StreamSession(const StreamSessionConnectInfo &connect_info, QObje throw ChiakiException("Morning invalid"); memcpy(chiaki_connect_info.morning, connect_info.morning.constData(), sizeof(chiaki_connect_info.morning)); - memset(&keyboard_state, 0, sizeof(keyboard_state)); + chiaki_controller_state_set_idle(&keyboard_state); ChiakiErrorCode err = chiaki_session_init(&session, &chiaki_connect_info, log.GetChiakiLog()); if(err != CHIAKI_ERR_SUCCESS) @@ -88,6 +91,16 @@ StreamSession::StreamSession(const StreamSessionConnectInfo &connect_info, QObje connect(ControllerManager::GetInstance(), &ControllerManager::AvailableControllersUpdated, this, &StreamSession::UpdateGamepads); #endif +#if CHIAKI_GUI_ENABLE_SETSU + chiaki_controller_state_set_idle(&setsu_state); + setsu = setsu_new(); + auto timer = new QTimer(this); + connect(timer, &QTimer::timeout, this, [this]{ + setsu_poll(setsu, SessionSetsuCb, this); + }); + timer->start(SETSU_UPDATE_INTERVAL_MS); +#endif + key_map = connect_info.key_map; UpdateGamepads(); } @@ -100,6 +113,9 @@ StreamSession::~StreamSession() #if CHIAKI_GUI_ENABLE_SDL_GAMECONTROLLER delete controller; #endif +#if CHIAKI_GUI_ENABLE_SETSU + setsu_free(setsu); +#endif } void StreamSession::Start() @@ -217,13 +233,18 @@ void StreamSession::UpdateGamepads() void StreamSession::SendFeedbackState() { - ChiakiControllerState state = {}; + ChiakiControllerState state; + chiaki_controller_state_set_idle(&state); #if CHIAKI_GUI_ENABLE_SDL_GAMECONTROLLER if(controller) state = controller->GetState(); #endif +#if CHIAKI_GUI_ENABLE_SETSU + chiaki_controller_state_or(&state, &state, &setsu_state); +#endif + chiaki_controller_state_or(&state, &state, &keyboard_state); chiaki_session_set_controller_state(&session, &state); } @@ -275,6 +296,8 @@ void StreamSession::Event(ChiakiEvent *event) { switch(event->type) { + case CHIAKI_EVENT_CONNECTED: + break; case CHIAKI_EVENT_QUIT: emit SessionQuit(event->quit.reason, event->quit.reason_str ? QString::fromUtf8(event->quit.reason_str) : QString()); break; @@ -284,6 +307,63 @@ void StreamSession::Event(ChiakiEvent *event) } } +#if CHIAKI_GUI_ENABLE_SETSU +void StreamSession::HandleSetsuEvent(SetsuEvent *event) +{ + if(!setsu) + return; + switch(event->type) + { + case SETSU_EVENT_DEVICE_ADDED: + setsu_connect(setsu, event->path); + break; + case SETSU_EVENT_DEVICE_REMOVED: + for(auto it=setsu_ids.begin(); it!=setsu_ids.end();) + { + if(it.key().first == event->path) + { + chiaki_controller_state_stop_touch(&setsu_state, it.value()); + setsu_ids.erase(it++); + } + else + it++; + } + SendFeedbackState(); + break; + case SETSU_EVENT_DOWN: + break; + case SETSU_EVENT_UP: + for(auto it=setsu_ids.begin(); it!=setsu_ids.end(); it++) + { + if(it.key().first == setsu_device_get_path(event->dev) && it.key().second == event->tracking_id) + { + chiaki_controller_state_stop_touch(&setsu_state, it.value()); + setsu_ids.erase(it); + break; + } + } + SendFeedbackState(); + break; + case SETSU_EVENT_POSITION: { + QPair k = { setsu_device_get_path(event->dev), event->tracking_id }; + auto it = setsu_ids.find(k); + if(it == setsu_ids.end()) + { + int8_t cid = chiaki_controller_state_start_touch(&setsu_state, event->x, event->y); + if(cid >= 0) + setsu_ids[k] = (uint8_t)cid; + else + break; + } + else + chiaki_controller_state_set_touch_pos(&setsu_state, it.value(), event->x, event->y); + SendFeedbackState(); + break; + } + } +} +#endif + class StreamSessionPrivate { public: @@ -295,6 +375,9 @@ class StreamSessionPrivate static void PushAudioFrame(StreamSession *session, int16_t *buf, size_t samples_count) { session->PushAudioFrame(buf, samples_count); } static void PushVideoSample(StreamSession *session, uint8_t *buf, size_t buf_size) { session->PushVideoSample(buf, buf_size); } static void Event(StreamSession *session, ChiakiEvent *event) { session->Event(event); } +#if CHIAKI_GUI_ENABLE_SETSU + static void HandleSetsuEvent(StreamSession *session, SetsuEvent *event) { session->HandleSetsuEvent(event); } +#endif }; static void AudioSettingsCb(uint32_t channels, uint32_t rate, void *user) @@ -321,3 +404,11 @@ static void EventCb(ChiakiEvent *event, void *user) auto session = reinterpret_cast(user); StreamSessionPrivate::Event(session, event); } + +#if CHIAKI_GUI_ENABLE_SETSU +static void SessionSetsuCb(SetsuEvent *event, void *user) +{ + auto session = reinterpret_cast(user); + StreamSessionPrivate::HandleSetsuEvent(session, event); +} +#endif diff --git a/lib/src/controller.c b/lib/src/controller.c index a50cac4..222b380 100644 --- a/lib/src/controller.c +++ b/lib/src/controller.c @@ -22,10 +22,19 @@ CHIAKI_EXPORT void chiaki_controller_state_set_idle(ChiakiControllerState *state) { state->buttons = 0; + state->l2_state = 0; + state->r2_state = 0; state->left_x = 0; state->left_y = 0; state->right_x = 0; state->right_y = 0; + state->touch_id_next = 0; + for(size_t i=0; itouches[i].id = -1; + state->touches[i].x = 0; + state->touches[i].y = 0; + } } CHIAKI_EXPORT int8_t chiaki_controller_state_start_touch(ChiakiControllerState *state, uint16_t x, uint16_t y) @@ -38,7 +47,7 @@ CHIAKI_EXPORT int8_t chiaki_controller_state_start_touch(ChiakiControllerState * state->touch_id_next = (state->touch_id_next + 1) & TOUCH_ID_MASK; state->touches[i].x = x; state->touches[i].y = y; - break; + return state->touches[i].id; } } return -1; @@ -87,13 +96,14 @@ CHIAKI_EXPORT void chiaki_controller_state_or(ChiakiControllerState *out, Chiaki out->touch_id_next = 0; for(size_t i=0; itouches[i].id >= 0 ? &a->touches[i] : b->touches[i].id >= 0 ? &b->touches[i] : NULL; + ChiakiControllerTouch *touch = a->touches[i].id >= 0 ? &a->touches[i] : (b->touches[i].id >= 0 ? &b->touches[i] : NULL); if(!touch) { out->touches[i].id = -1; out->touches[i].x = out->touches[i].y = 0; continue; } - out->touches[i] = *touch; + if(touch != &out->touches[i]) + out->touches[i] = *touch; } } From e0db330924fe42a1ad372f5132f67314f841de62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Fri, 3 Jul 2020 14:55:16 +0200 Subject: [PATCH 023/237] Fix Pointer Id in Feedback History --- lib/src/feedback.c | 2 +- lib/src/feedbacksender.c | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/src/feedback.c b/lib/src/feedback.c index d7a8b49..ce9a9bf 100644 --- a/lib/src/feedback.c +++ b/lib/src/feedback.c @@ -124,7 +124,7 @@ CHIAKI_EXPORT void chiaki_feedback_history_event_set_touchpad(ChiakiFeedbackHist { event->len = 5; event->buf[0] = down ? 0xd0 : 0xc0; - event->buf[1] = pointer_id / 0x7f; + event->buf[1] = pointer_id & 0x7f; event->buf[2] = (uint8_t)(x >> 4); event->buf[3] = (uint8_t)((x & 0xf) << 4) | (uint8_t)(y >> 8); event->buf[4] = (uint8_t)y; diff --git a/lib/src/feedbacksender.c b/lib/src/feedbacksender.c index 2fee4b6..adddd44 100644 --- a/lib/src/feedbacksender.c +++ b/lib/src/feedbacksender.c @@ -275,4 +275,4 @@ static void *feedback_sender_thread_func(void *user) chiaki_mutex_unlock(&feedback_sender->state_mutex); return NULL; -} \ No newline at end of file +} From b8f572842cc27fdafddd8541fe49ccf2081d746c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Fri, 3 Jul 2020 14:56:42 +0200 Subject: [PATCH 024/237] Fix GUI build without Setsu --- gui/src/streamsession.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/gui/src/streamsession.cpp b/gui/src/streamsession.cpp index dd4104a..45cfc3a 100644 --- a/gui/src/streamsession.cpp +++ b/gui/src/streamsession.cpp @@ -46,7 +46,9 @@ static void AudioSettingsCb(uint32_t channels, uint32_t rate, void *user); static void AudioFrameCb(int16_t *buf, size_t samples_count, void *user); static bool VideoSampleCb(uint8_t *buf, size_t buf_size, void *user); static void EventCb(ChiakiEvent *event, void *user); +#if CHIAKI_GUI_ENABLE_SETSU static void SessionSetsuCb(SetsuEvent *event, void *user); +#endif StreamSession::StreamSession(const StreamSessionConnectInfo &connect_info, QObject *parent) : QObject(parent), From 686e2a7a9b842e693e3878f2aa3caca21ff43923 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Fri, 3 Jul 2020 16:00:31 +0200 Subject: [PATCH 025/237] Support Bluetooth Connection in Setsu --- setsu/src/setsu.c | 54 +++++++++++++++++++++++++++++++++++++---------- 1 file changed, 43 insertions(+), 11 deletions(-) diff --git a/setsu/src/setsu.c b/setsu/src/setsu.c index bfce543..252d4c6 100644 --- a/setsu/src/setsu.c +++ b/setsu/src/setsu.c @@ -79,6 +79,7 @@ struct setsu_t SetsuDevice *dev; }; +bool get_dev_ids(const char *path, uint32_t *vendor_id, uint32_t *model_id); static void scan_udev(Setsu *setsu); static void update_udev_device(Setsu *setsu, struct udev_device *dev); static SetsuDevice *connect(Setsu *setsu, const char *path); @@ -166,25 +167,38 @@ beach: static bool is_device_interesting(struct udev_device *dev) { - static const char *device_ids[] = { + static const uint32_t device_ids[] = { // vendor id, model id - "054c", "05c4", // DualShock 4 Gen 1 USB - "054c", "09cc", // DualShock 4 Gen 2 USB - NULL + 0x054c, 0x05c4, // DualShock 4 Gen 1 USB + 0x054c, 0x09cc // DualShock 4 Gen 2 USB }; // Filter mouse-device (/dev/input/mouse*) away and only keep the evdev (/dev/input/event*) one: if(!udev_device_get_property_value(dev, "ID_INPUT_TOUCHPAD_INTEGRATION")) return false; - const char *vendor = udev_device_get_property_value(dev, "ID_VENDOR_ID"); - const char *model = udev_device_get_property_value(dev, "ID_MODEL_ID"); - if(!vendor || !model) - return false; - - for(const char **dev_id = device_ids; *dev_id; dev_id += 2) + uint32_t vendor; + uint32_t model; + // Try to get the ids from udev first. This fails for bluetooth. + const char *vendor_str = udev_device_get_property_value(dev, "ID_VENDOR_ID"); + const char *model_str = udev_device_get_property_value(dev, "ID_MODEL_ID"); + if(vendor_str && model_str) { - if(!strcmp(vendor, dev_id[0]) && !strcmp(model, dev_id[1])) + vendor = strtoul(vendor_str, NULL, 16); + model = strtoul(model_str, NULL, 16); + } + else + { + const char *path = udev_device_get_devnode(dev); + if(!path) + return false; + if(!get_dev_ids(path, &vendor, &model)) + return false; + } + + for(const uint32_t *dev_id = device_ids; (char *)dev_id != ((char *)device_ids) + sizeof(device_ids); dev_id += 2) + { + if(vendor == dev_id[0] && model == dev_id[1]) return true; } return false; @@ -247,6 +261,24 @@ static void poll_udev_monitor(Setsu *setsu) } } +bool get_dev_ids(const char *path, uint32_t *vendor_id, uint32_t *model_id) +{ + int fd = open(path, O_RDONLY | O_NONBLOCK); + if(fd == -1) + return false; + + struct libevdev *evdev; + if(libevdev_new_from_fd(fd, &evdev) < 0) + { + close(fd); + return false; + } + + *vendor_id = (uint32_t)libevdev_get_id_vendor(evdev); + *model_id = (uint32_t)libevdev_get_id_product(evdev); + return true; +} + SetsuDevice *setsu_connect(Setsu *setsu, const char *path) { SetsuDevice *dev = calloc(1, sizeof(SetsuDevice)); From da1ca629430e56bbad308c3feaae673ffb1108cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Fri, 3 Jul 2020 16:02:22 +0200 Subject: [PATCH 026/237] Rename Setsu test to demo --- setsu/CMakeLists.txt | 10 +++++----- setsu/{test => demo}/main.c | 0 2 files changed, 5 insertions(+), 5 deletions(-) rename setsu/{test => demo}/main.c (100%) diff --git a/setsu/CMakeLists.txt b/setsu/CMakeLists.txt index 3c63a9f..90c1465 100644 --- a/setsu/CMakeLists.txt +++ b/setsu/CMakeLists.txt @@ -3,7 +3,7 @@ cmake_minimum_required(VERSION 3.2) project(libsetsu) -option(SETSU_BUILD_TEST "Build testing executable for libsetsu" OFF) +option(SETSU_BUILD_DEMO "Build testing executable for libsetsu" OFF) list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake") @@ -17,9 +17,9 @@ find_package(Udev REQUIRED) find_package(Evdev REQUIRED) target_link_libraries(setsu Udev::libudev Evdev::libevdev) -if(SETSU_BUILD_TEST) - add_executable(setsutest - test/main.c) - target_link_libraries(setsutest setsu) +if(SETSU_BUILD_DEMO) + add_executable(setsu-demo + demo/main.c) + target_link_libraries(setsu-demo setsu) endif() diff --git a/setsu/test/main.c b/setsu/demo/main.c similarity index 100% rename from setsu/test/main.c rename to setsu/demo/main.c From fd62af5b6ccba30a503e61f860986882fc3cc8b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Fri, 3 Jul 2020 17:16:09 +0200 Subject: [PATCH 027/237] Fix Setsu Disconnect for evdev id check --- setsu/src/setsu.c | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/setsu/src/setsu.c b/setsu/src/setsu.c index 252d4c6..fdca7b2 100644 --- a/setsu/src/setsu.c +++ b/setsu/src/setsu.c @@ -206,8 +206,6 @@ static bool is_device_interesting(struct udev_device *dev) static void update_udev_device(Setsu *setsu, struct udev_device *dev) { - if(!is_device_interesting(dev)) - return; const char *path = udev_device_get_devnode(dev); if(!path) return; @@ -232,7 +230,11 @@ static void update_udev_device(Setsu *setsu, struct udev_device *dev) return; } } + // not yet added + if(!is_device_interesting(dev)) + return; + SetsuAvailDevice *adev = calloc(1, sizeof(SetsuAvailDevice)); if(!adev) return; From 8bdf1a000328c27e80bda3e2cfb1f530fa2b4ad6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Fri, 3 Jul 2020 17:23:41 +0200 Subject: [PATCH 028/237] Fix a leak in Setsu --- setsu/src/setsu.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/setsu/src/setsu.c b/setsu/src/setsu.c index fdca7b2..25be3a1 100644 --- a/setsu/src/setsu.c +++ b/setsu/src/setsu.c @@ -278,6 +278,8 @@ bool get_dev_ids(const char *path, uint32_t *vendor_id, uint32_t *model_id) *vendor_id = (uint32_t)libevdev_get_id_vendor(evdev); *model_id = (uint32_t)libevdev_get_id_product(evdev); + libevdev_free(evdev); + close(fd); return true; } From abec268ab69492899b971363813425c648f15621 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Fri, 3 Jul 2020 18:18:50 +0200 Subject: [PATCH 029/237] Add Buttons to Setsu --- setsu/demo/main.c | 17 ++++--- setsu/include/setsu.h | 27 ++++++++-- setsu/src/setsu.c | 114 +++++++++++++++++++++++++++++------------- 3 files changed, 112 insertions(+), 46 deletions(-) diff --git a/setsu/demo/main.c b/setsu/demo/main.c index be36535..a1167e7 100644 --- a/setsu/demo/main.c +++ b/setsu/demo/main.c @@ -90,7 +90,7 @@ void event(SetsuEvent *event, void *user) case SETSU_EVENT_DEVICE_REMOVED: LOG("Device removed: %s\n", event->path); break; - case SETSU_EVENT_DOWN: + case SETSU_EVENT_TOUCH_DOWN: LOG("Down for %s, tracking id %d\n", setsu_device_get_path(event->dev), event->tracking_id); for(size_t i=0; itype == SETSU_EVENT_UP) + case SETSU_EVENT_TOUCH_POSITION: + case SETSU_EVENT_TOUCH_UP: + if(event->type == SETSU_EVENT_TOUCH_UP) LOG("Up for %s, tracking id %d\n", setsu_device_get_path(event->dev), event->tracking_id); else LOG("Position for %s, tracking id %d: %u, %u\n", setsu_device_get_path(event->dev), @@ -115,11 +115,11 @@ void event(SetsuEvent *event, void *user) { switch(event->type) { - case SETSU_EVENT_POSITION: + case SETSU_EVENT_TOUCH_POSITION: touches[i].x = event->x; touches[i].y = event->y; break; - case SETSU_EVENT_UP: + case SETSU_EVENT_TOUCH_UP: touches[i].down = false; break; default: @@ -128,6 +128,11 @@ void event(SetsuEvent *event, void *user) } } break; + case SETSU_EVENT_BUTTON_DOWN: + case SETSU_EVENT_BUTTON_UP: + LOG("Button for %s: %llu %s\n", setsu_device_get_path(event->dev), + (unsigned long long)event->button, event->type == SETSU_EVENT_BUTTON_DOWN ? "down" : "up"); + break; } } diff --git a/setsu/include/setsu.h b/setsu/include/setsu.h index 0303024..a83f1a1 100644 --- a/setsu/include/setsu.h +++ b/setsu/include/setsu.h @@ -42,17 +42,27 @@ typedef enum { /* Touch down. * Event will have dev and tracking_id set. */ - SETSU_EVENT_DOWN, + SETSU_EVENT_TOUCH_DOWN, /* Touch down. * Event will have dev and tracking_id set. */ - SETSU_EVENT_UP, + SETSU_EVENT_TOUCH_UP, /* Touch position update. * Event will have dev, tracking_id, x and y set. */ - SETSU_EVENT_POSITION + SETSU_EVENT_TOUCH_POSITION, + + /* Event will have dev and button set. */ + SETSU_EVENT_BUTTON_DOWN, + + /* Event will have dev and button set. */ + SETSU_EVENT_BUTTON_UP } SetsuEventType; +#define SETSU_BUTTON_0 (1u << 0) + +typedef uint64_t SetsuButton; + typedef struct setsu_event_t { SetsuEventType type; @@ -62,8 +72,15 @@ typedef struct setsu_event_t struct { SetsuDevice *dev; - SetsuTrackingId tracking_id; - uint32_t x, y; + union + { + struct + { + SetsuTrackingId tracking_id; + uint32_t x, y; + }; + SetsuButton button; + }; }; }; } SetsuEvent; diff --git a/setsu/src/setsu.c b/setsu/src/setsu.c index 25be3a1..00ab5bb 100644 --- a/setsu/src/setsu.c +++ b/setsu/src/setsu.c @@ -69,6 +69,9 @@ typedef struct setsu_device_t bool pos_dirty; } slots[SLOTS_COUNT]; unsigned int slot_cur; + + uint64_t buttons_prev; + uint64_t buttons_cur; } SetsuDevice; struct setsu_t @@ -458,6 +461,17 @@ static void poll_device(Setsu *setsu, SetsuDevice *dev, SetsuEventCb cb, void *u } } +static uint64_t button_from_evdev(int key) +{ + switch(key) + { + case BTN_LEFT: + return SETSU_BUTTON_0; + default: + return 0; + } +} + static void device_event(Setsu *setsu, SetsuDevice *dev, struct input_event *ev, SetsuEventCb cb, void *user) { #if 0 @@ -467,43 +481,57 @@ static void device_event(Setsu *setsu, SetsuDevice *dev, struct input_event *ev, ev->value); #endif #define S dev->slots[dev->slot_cur] - if(ev->type == EV_ABS) + switch(ev->type) { - switch(ev->code) - { - case ABS_MT_SLOT: - if((unsigned int)ev->value >= SLOTS_COUNT) - { - SETSU_LOG("slot too high\n"); + case EV_ABS: + switch(ev->code) + { + case ABS_MT_SLOT: + if((unsigned int)ev->value >= SLOTS_COUNT) + { + SETSU_LOG("slot too high\n"); + break; + } + dev->slot_cur = ev->value; break; - } - dev->slot_cur = ev->value; - break; - case ABS_MT_TRACKING_ID: - if(S.tracking_id != -1 && S.tracking_id_prev == -1) - { - // up the tracking id - S.tracking_id_prev = S.tracking_id; - // reset the rest - S.x = S.y = 0; - S.pos_dirty = false; - } - S.tracking_id = ev->value; - if(ev->value != -1) - S.downed = true; - break; - case ABS_MT_POSITION_X: - S.x = ev->value; - S.pos_dirty = true; - break; - case ABS_MT_POSITION_Y: - S.y = ev->value; - S.pos_dirty = true; + case ABS_MT_TRACKING_ID: + if(S.tracking_id != -1 && S.tracking_id_prev == -1) + { + // up the tracking id + S.tracking_id_prev = S.tracking_id; + // reset the rest + S.x = S.y = 0; + S.pos_dirty = false; + } + S.tracking_id = ev->value; + if(ev->value != -1) + S.downed = true; + break; + case ABS_MT_POSITION_X: + S.x = ev->value; + S.pos_dirty = true; + break; + case ABS_MT_POSITION_Y: + S.y = ev->value; + S.pos_dirty = true; + break; + } + break; + case EV_KEY: { + uint64_t button = button_from_evdev(ev->code); + if(!button) break; + if(ev->value) + dev->buttons_cur |= button; + else + dev->buttons_cur &= ~button; + break; } + case EV_SYN: + if(ev->code == SYN_REPORT) + device_drain(setsu, dev, cb, user); + break; } - else if(ev->type == EV_SYN && ev->code == SYN_REPORT) - device_drain(setsu, dev, cb, user); #undef S } @@ -516,21 +544,21 @@ static void device_drain(Setsu *setsu, SetsuDevice *dev, SetsuEventCb cb, void * { if(dev->slots[i].tracking_id_prev != -1) { - BEGIN_EVENT(SETSU_EVENT_UP); + BEGIN_EVENT(SETSU_EVENT_TOUCH_UP); event.tracking_id = dev->slots[i].tracking_id_prev; SEND_EVENT(); dev->slots[i].tracking_id_prev = -1; } if(dev->slots[i].downed) { - BEGIN_EVENT(SETSU_EVENT_DOWN); + BEGIN_EVENT(SETSU_EVENT_TOUCH_DOWN); event.tracking_id = dev->slots[i].tracking_id; SEND_EVENT(); dev->slots[i].downed = false; } if(dev->slots[i].pos_dirty) { - BEGIN_EVENT(SETSU_EVENT_POSITION); + BEGIN_EVENT(SETSU_EVENT_TOUCH_POSITION); event.tracking_id = dev->slots[i].tracking_id; event.x = (uint32_t)(dev->slots[i].x - dev->min_x); event.y = (uint32_t)(dev->slots[i].y - dev->min_y); @@ -538,6 +566,22 @@ static void device_drain(Setsu *setsu, SetsuDevice *dev, SetsuEventCb cb, void * dev->slots[i].pos_dirty = false; } } + + uint64_t buttons_diff = dev->buttons_prev ^ dev->buttons_cur; + for(uint64_t i=0; i<64; i++) + { + if(buttons_diff & 1) + { + uint64_t button = 1 << i; + BEGIN_EVENT((dev->buttons_cur & button) ? SETSU_EVENT_BUTTON_DOWN : SETSU_EVENT_BUTTON_UP); + event.button = button; + SEND_EVENT(); + } + buttons_diff >>= 1; + if(!buttons_diff) + break; + } + dev->buttons_prev = dev->buttons_cur; #undef BEGIN_EVENT #undef SEND_EVENT } From eac18f81e0695a7155618cdb8df9a717b78bc757 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Fri, 3 Jul 2020 18:23:00 +0200 Subject: [PATCH 030/237] Use Button from Setsu in GUI --- gui/src/streamsession.cpp | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/gui/src/streamsession.cpp b/gui/src/streamsession.cpp index 45cfc3a..dd4ddfd 100644 --- a/gui/src/streamsession.cpp +++ b/gui/src/streamsession.cpp @@ -332,9 +332,9 @@ void StreamSession::HandleSetsuEvent(SetsuEvent *event) } SendFeedbackState(); break; - case SETSU_EVENT_DOWN: + case SETSU_EVENT_TOUCH_DOWN: break; - case SETSU_EVENT_UP: + case SETSU_EVENT_TOUCH_UP: for(auto it=setsu_ids.begin(); it!=setsu_ids.end(); it++) { if(it.key().first == setsu_device_get_path(event->dev) && it.key().second == event->tracking_id) @@ -346,7 +346,7 @@ void StreamSession::HandleSetsuEvent(SetsuEvent *event) } SendFeedbackState(); break; - case SETSU_EVENT_POSITION: { + case SETSU_EVENT_TOUCH_POSITION: { QPair k = { setsu_device_get_path(event->dev), event->tracking_id }; auto it = setsu_ids.find(k); if(it == setsu_ids.end()) @@ -362,6 +362,12 @@ void StreamSession::HandleSetsuEvent(SetsuEvent *event) SendFeedbackState(); break; } + case SETSU_EVENT_BUTTON_DOWN: + setsu_state.buttons |= CHIAKI_CONTROLLER_BUTTON_TOUCHPAD; + break; + case SETSU_EVENT_BUTTON_UP: + setsu_state.buttons &= ~CHIAKI_CONTROLLER_BUTTON_TOUCHPAD; + break; } } #endif From 68aadd4343bae914ee09432551410639ac607358 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Sat, 4 Jul 2020 12:06:14 +0200 Subject: [PATCH 031/237] Make initial udev/evdev checks QUIET --- CMakeLists.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 9e3f063..f7049a1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -71,8 +71,8 @@ if(CHIAKI_ENABLE_CLI) endif() if(CHIAKI_ENABLE_SETSU) - find_package(Udev) - find_package(Evdev) + find_package(Udev QUIET) + find_package(Evdev QUIET) if(Udev_FOUND AND Evdev_FOUND) set(CHIAKI_ENABLE_SETSU ON) else() From 736b4835dfee77e34c09faa2623612691f7c1489 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Sun, 12 Jul 2020 19:28:36 +0200 Subject: [PATCH 032/237] Add Key State Tracker --- lib/include/chiaki/gkcrypt.h | 7 +++++ lib/src/gkcrypt.c | 16 ++++++++++ test/CMakeLists.txt | 1 + test/keystate.c | 57 ++++++++++++++++++++++++++++++++++++ test/main.c | 8 +++++ 5 files changed, 89 insertions(+) create mode 100644 test/keystate.c diff --git a/lib/include/chiaki/gkcrypt.h b/lib/include/chiaki/gkcrypt.h index a2c76e7..4573810 100644 --- a/lib/include/chiaki/gkcrypt.h +++ b/lib/include/chiaki/gkcrypt.h @@ -96,6 +96,13 @@ static inline void chiaki_gkcrypt_free(ChiakiGKCrypt *gkcrypt) free(gkcrypt); } +typedef struct chiaki_key_state_t { + uint64_t prev; +} ChiakiKeyState; + +CHIAKI_EXPORT void chiaki_key_state_init(ChiakiKeyState *state); +CHIAKI_EXPORT uint64_t chiaki_key_state_request_pos(ChiakiKeyState *state, uint32_t low); + #ifdef __cplusplus } #endif diff --git a/lib/src/gkcrypt.c b/lib/src/gkcrypt.c index 0a2bc15..b398a28 100644 --- a/lib/src/gkcrypt.c +++ b/lib/src/gkcrypt.c @@ -542,3 +542,19 @@ static void *gkcrypt_thread_func(void *user) chiaki_mutex_unlock(&gkcrypt->key_buf_mutex); return NULL; } + +CHIAKI_EXPORT void chiaki_key_state_init(ChiakiKeyState *state) +{ + state->prev = 0; +} + +CHIAKI_EXPORT uint64_t chiaki_key_state_request_pos(ChiakiKeyState *state, uint32_t low) +{ + uint32_t prev_low = (uint32_t)state->prev; + uint32_t high = (uint32_t)(state->prev >> 32); + if(chiaki_seq_num_32_gt(low, prev_low) && low < prev_low) + high++; + else if(chiaki_seq_num_32_lt(low, prev_low) && low > prev_low && high) + high--; + return state->prev = (((uint64_t)high) << 32) | ((uint64_t)low); +} diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 52bc34a..827f4a7 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -9,6 +9,7 @@ add_executable(chiaki-unit gkcrypt.c takion.c seqnum.c + keystate.c reorderqueue.c fec.c test_log.c diff --git a/test/keystate.c b/test/keystate.c new file mode 100644 index 0000000..97bb6f2 --- /dev/null +++ b/test/keystate.c @@ -0,0 +1,57 @@ +/* + * This file is part of Chiaki. + * + * Chiaki is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Chiaki is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Chiaki. If not, see . + */ + +#include + +#include + +static MunitResult test_key_state(const MunitParameter params[], void *user) +{ + ChiakiKeyState state; + chiaki_key_state_init(&state); + + uint64_t pos = chiaki_key_state_request_pos(&state, 0); + munit_assert_uint64(pos, ==, 0); + pos = chiaki_key_state_request_pos(&state, 0x1337); + munit_assert_uint64(pos, ==, 0x1337); + pos = chiaki_key_state_request_pos(&state, 0xffff0000); + munit_assert_uint64(pos, ==, 0xffff0000); + pos = chiaki_key_state_request_pos(&state, 0x1337); + munit_assert_uint64(pos, ==, 0x100001337); + pos = chiaki_key_state_request_pos(&state, 0xffff1337); + munit_assert_uint64(pos, ==, 0xffff1337); + pos = chiaki_key_state_request_pos(&state, 0x50000000); + munit_assert_uint64(pos, ==, 0x150000000); + pos = chiaki_key_state_request_pos(&state, 0xb0000000); + munit_assert_uint64(pos, ==, 0x1b0000000); + pos = chiaki_key_state_request_pos(&state, 0x00000000); + munit_assert_uint64(pos, ==, 0x200000000); + + return MUNIT_OK; +} + +MunitTest tests_key_state[] = { + { + "/key_state", + test_key_state, + NULL, + NULL, + MUNIT_TEST_OPTION_NONE, + NULL + }, + { NULL, NULL, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL } +}; diff --git a/test/main.c b/test/main.c index 299382f..6a43def 100644 --- a/test/main.c +++ b/test/main.c @@ -18,6 +18,7 @@ #include extern MunitTest tests_seq_num[]; +extern MunitTest tests_key_state[]; extern MunitTest tests_reorder_queue[]; extern MunitTest tests_http[]; extern MunitTest tests_rpcrypt[]; @@ -34,6 +35,13 @@ static MunitSuite suites[] = { 1, MUNIT_SUITE_OPTION_NONE }, + { + "/key_state", + tests_key_state, + NULL, + 1, + MUNIT_SUITE_OPTION_NONE + }, { "/reorder_queue", tests_reorder_queue, From d4b4681a0f6a527620ace84ba24ffa1df08ce258 Mon Sep 17 00:00:00 2001 From: Ritiek Malhotra Date: Fri, 17 Jul 2020 15:04:18 +0530 Subject: [PATCH 033/237] Show expected & received length in error messages (#277) --- gui/src/main.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/gui/src/main.cpp b/gui/src/main.cpp index 3a2b449..fc011d5 100644 --- a/gui/src/main.cpp +++ b/gui/src/main.cpp @@ -116,7 +116,9 @@ int real_main(int argc, char *argv[]) QByteArray regist_key = parser.value(regist_key_option).toUtf8(); if(regist_key.length() > sizeof(ChiakiConnectInfo::regist_key)) { - printf("Given regist key is too long.\n"); + printf("Given regist key is too long (expected size <=%llu, got %d)\n", + (unsigned long long)sizeof(ChiakiConnectInfo::regist_key), + regist_key.length()); return 1; } regist_key += QByteArray(sizeof(ChiakiConnectInfo::regist_key) - regist_key.length(), 0); @@ -124,6 +126,9 @@ int real_main(int argc, char *argv[]) QByteArray morning = QByteArray::fromBase64(parser.value(morning_option).toUtf8()); if(morning.length() != sizeof(ChiakiConnectInfo::morning)) { + printf("Given morning has invalid size (expected %llu, got %d)\n", + (unsigned long long)sizeof(ChiakiConnectInfo::morning), + morning.length()); printf("Given morning has invalid size (expected %llu)", (unsigned long long)sizeof(ChiakiConnectInfo::morning)); return 1; } From a9e50e46ea16ad13a86229483d61678477c6f022 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Thu, 13 Aug 2020 18:51:29 +0200 Subject: [PATCH 034/237] Use prebuilt Docker Image for Switch CI (#301) --- .github/workflows/switch.yml | 2 - scripts/switch/Dockerfile | 80 ----------------------- scripts/switch/build-docker-image.sh | 5 -- scripts/switch/devkit_repo | 6 -- scripts/switch/run-docker-build-chiaki.sh | 2 +- 5 files changed, 1 insertion(+), 94 deletions(-) delete mode 100644 scripts/switch/Dockerfile delete mode 100755 scripts/switch/build-docker-image.sh delete mode 100644 scripts/switch/devkit_repo diff --git a/.github/workflows/switch.yml b/.github/workflows/switch.yml index 944b40e..7c5effe 100644 --- a/.github/workflows/switch.yml +++ b/.github/workflows/switch.yml @@ -11,8 +11,6 @@ jobs: run: | git submodule init git submodule update - - name: Docker Build - run: scripts/switch/build-docker-image.sh - name: Build Chiaki run: scripts/switch/run-docker-build-chiaki.sh diff --git a/scripts/switch/Dockerfile b/scripts/switch/Dockerfile deleted file mode 100644 index 494013d..0000000 --- a/scripts/switch/Dockerfile +++ /dev/null @@ -1,80 +0,0 @@ -FROM archlinux:latest - -ENV DEVKITPRO=/opt/devkitpro -ENV DEVKITARM=/opt/devkitpro/devkitARM -ENV DEVKITPPC=/opt/devkitpro/devkitPPC -ENV PATH="${PATH}:${DEVKITARM}/bin/:${DEVKITPPC}/bin/" - -ENV WORKDIR="/build" -WORKDIR "${WORKDIR}" - -# Upgrade image -RUN pacman --noconfirm -Syu - -# Install requirements for libtransistor -RUN pacman --noconfirm -S \ - llvm \ - clang \ - lld \ - python \ - python-pip \ - python-virtualenv \ - squashfs-tools \ - base-devel \ - git \ - cmake \ - libx11 \ - vim - -RUN pacman-key --init -# Install devkitpro -# doc source : -# https://devkitpro.org/wiki/devkitPro_pacman - -# First import the key which is used to validate the packages -RUN pacman-key --recv-keys F7FD5492264BB9D0 -RUN pacman-key --lsign F7FD5492264BB9D0 - -# Add the devkitPro repositories -ADD devkit_repo ./devkit_repo -RUN cat ./devkit_repo >> /etc/pacman.conf -# Install the keyring which adds more keys which may be used to verify the packages. -RUN pacman --noconfirm -U https://downloads.devkitpro.org/devkitpro-keyring-r1.787e015-2-any.pkg.tar.xz -# Now resync the database and update installed packages. -RUN pacman -Sy - -RUN pacman --noconfirm -Syu - -#RUN pacman --noconfirm -S $(pacman -Slq dkp-libs) - -RUN pacman --noconfirm -S \ - protobuf \ - python-protobuf \ - sfml \ - devkitARM \ - switch-pkg-config \ - devkitpro-pkgbuild-helpers \ - switch-dev \ - switch-zlib \ - switch-sdl2 \ - switch-freetype \ - switch-curl \ - switch-mesa \ - switch-glad \ - switch-glm \ - switch-libconfig \ - switch-sdl2_gfx \ - switch-sdl2_ttf \ - switch-sdl2_image \ - switch-libexpat \ - switch-bzip2 \ - switch-libopus \ - switch-ffmpeg \ - switch-mbedtls - -# RUN pip3 install -U pip - -VOLUME ${WORKDIR} -# nxlink server port -EXPOSE 28771 -ENTRYPOINT ["/bin/bash"] diff --git a/scripts/switch/build-docker-image.sh b/scripts/switch/build-docker-image.sh deleted file mode 100755 index f8c7ea2..0000000 --- a/scripts/switch/build-docker-image.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/bash - -cd "`dirname $(readlink -f ${0})`" - -docker build "$@" -t chiaki-switch . diff --git a/scripts/switch/devkit_repo b/scripts/switch/devkit_repo deleted file mode 100644 index 2646e2d..0000000 --- a/scripts/switch/devkit_repo +++ /dev/null @@ -1,6 +0,0 @@ -[dkp-libs] -Server = http://downloads.devkitpro.org/packages - -[dkp-linux] -Server = http://downloads.devkitpro.org/packages/linux - diff --git a/scripts/switch/run-docker-build-chiaki.sh b/scripts/switch/run-docker-build-chiaki.sh index 660ab42..0fe7ad6 100755 --- a/scripts/switch/run-docker-build-chiaki.sh +++ b/scripts/switch/run-docker-build-chiaki.sh @@ -5,6 +5,6 @@ cd "`dirname $(readlink -f ${0})`/../.." docker run \ -v "`pwd`:/build/chiaki" \ -t \ - chiaki-switch \ + thestr4ng3r/chiaki-build-switch \ -c "cd /build/chiaki && scripts/switch/build.sh" From e8d15db8de0d21b737b06bec01257593cb816b4d Mon Sep 17 00:00:00 2001 From: Poussinou Date: Thu, 3 Sep 2020 13:32:34 +0200 Subject: [PATCH 035/237] Add F-Droid info to README.md (#308) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c4a2df8..0831ecf 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ Builds are provided for Linux, Android, macOS and Windows. You can download them [here](https://github.com/thestr4ng3r/chiaki/releases). * **Linux**: The provided file is an [AppImage](https://appimage.org/). Simply make it executable (`chmod +x .AppImage`) and run it. -* **Android**: Install from [Google Play](https://play.google.com/store/apps/details?id=com.metallic.chiaki) or download the APK from GitHub. +* **Android**: Install from [Google Play](https://play.google.com/store/apps/details?id=com.metallic.chiaki), [F-Droid](https://f-droid.org/packages/com.metallic.chiaki/) or download the APK from GitHub. * **macOS**: Drag the application from the `.dmg` into your Applications folder. * **Windows**: Extract the `.zip` file and execute `chiaki.exe`. From 8511a74c1049015901ac5dd405c7465380fca5c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Till=20M=C3=BCller?= Date: Sun, 6 Sep 2020 15:17:33 +0200 Subject: [PATCH 036/237] Fix MTU detection (Fix #303) (#304) --- lib/src/senkusha.c | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/lib/src/senkusha.c b/lib/src/senkusha.c index e45d39f..d1049fd 100644 --- a/lib/src/senkusha.c +++ b/lib/src/senkusha.c @@ -363,7 +363,7 @@ static ChiakiErrorCode senkusha_run_mtu_in_test(ChiakiSenkusha *senkusha, uint32 uint32_t cur = max; uint32_t request_id = 0; - while(max > min) + while((max - min) > 1) { bool success = false; for(uint32_t attempt=0; attemptlog, "Senkusha determined inbound MTU %u", (unsigned int)max); - *mtu = max; + CHIAKI_LOGI(senkusha->log, "Senkusha determined inbound MTU %u", (unsigned int)min); + *mtu = min; return CHIAKI_ERR_SUCCESS; } @@ -481,7 +481,7 @@ static ChiakiErrorCode senkusha_run_mtu_out_test(ChiakiSenkusha *senkusha, uint3 err = CHIAKI_ERR_SUCCESS; uint32_t cur = mtu_in; - while(max > min) + while((max - min) > 1) { bool success = false; for(uint32_t attempt=0; attemptlog, "Senkusha failed to send ping"); - goto beach; + err = CHIAKI_ERR_TIMEOUT; + } + else + { + err = chiaki_cond_timedwait_pred(&senkusha->state_cond, &senkusha->state_mutex, timeout_ms, state_finished_cond_check, senkusha); } - err = chiaki_cond_timedwait_pred(&senkusha->state_cond, &senkusha->state_mutex, timeout_ms, state_finished_cond_check, senkusha); assert(err == CHIAKI_ERR_SUCCESS || err == CHIAKI_ERR_TIMEOUT); if(!senkusha->state_finished) @@ -549,14 +552,14 @@ static ChiakiErrorCode senkusha_run_mtu_out_test(ChiakiSenkusha *senkusha, uint3 } if(success) - min = cur + 1; + min = cur; else - max = cur - 1; + max = cur; cur = min + (max - min) / 2; } - CHIAKI_LOGI(senkusha->log, "Senkusha determined outbound MTU %u", (unsigned int)max); - *mtu = max; + CHIAKI_LOGI(senkusha->log, "Senkusha determined outbound MTU %u", (unsigned int)min); + *mtu = min; CHIAKI_LOGI(senkusha->log, "Senkusha sending final Client MTU Command"); client_mtu_cmd.id = 2; From f1071f31b68e9150d8c2337156cd839802deed91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Sun, 6 Sep 2020 15:26:16 +0200 Subject: [PATCH 037/237] Remove sdl2-static from alpine deps --- .builds/alpine.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.builds/alpine.yml b/.builds/alpine.yml index 07066c9..88cb3ad 100644 --- a/.builds/alpine.yml +++ b/.builds/alpine.yml @@ -14,7 +14,6 @@ packages: - qt5-qtmultimedia-dev - ffmpeg-dev - sdl2-dev - - sdl2-static tasks: - build: | From 4dac2253df4b7e4d23658a35817158ecfcc76764 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Fri, 16 Oct 2020 11:01:00 +0200 Subject: [PATCH 038/237] PS4 Firmware 8.0 Compatibility (Fix #328) --- .gitignore | 1 + android/app/build.gradle | 1 + android/app/src/main/cpp/chiaki-jni.c | 6 + .../java/com/metallic/chiaki/lib/Chiaki.kt | 9 + .../metallic/chiaki/regist/RegistActivity.kt | 24 +- .../metallic/chiaki/regist/RegistViewModel.kt | 3 +- .../src/main/res/layout/activity_regist.xml | 13 +- android/app/src/main/res/values/strings.xml | 3 +- gui/include/registdialog.h | 7 +- gui/src/registdialog.cpp | 49 +- lib/include/chiaki/common.h | 9 + lib/include/chiaki/regist.h | 3 +- lib/include/chiaki/rpcrypt.h | 11 +- lib/include/chiaki/session.h | 14 +- lib/src/ctrl.c | 32 +- lib/src/regist.c | 69 +- lib/src/rpcrypt.c | 937 +++++++++++++++++- lib/src/session.c | 70 +- test/regist.c | 27 +- test/rpcrypt.c | 121 ++- 20 files changed, 1273 insertions(+), 136 deletions(-) diff --git a/.gitignore b/.gitignore index 5e82c02..bb63e63 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,4 @@ secret.tar keystore-env.sh compile_commands.json .ccls-cache +.gdb_history diff --git a/android/app/build.gradle b/android/app/build.gradle index a83e35a..62e5984 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -31,6 +31,7 @@ android { arguments "-DCHIAKI_ENABLE_TESTS=OFF", "-DCHIAKI_ENABLE_CLI=OFF", "-DCHIAKI_ENABLE_GUI=OFF", + "-DCHIAKI_ENABLE_SETSU=OFF", "-DCHIAKI_ENABLE_ANDROID=ON", "-DCHIAKI_LIB_ENABLE_OPUS=OFF", "-DCHIAKI_LIB_OPENSSL_EXTERNAL_PROJECT=ON" diff --git a/android/app/src/main/cpp/chiaki-jni.c b/android/app/src/main/cpp/chiaki-jni.c index e0884ce..feab1a9 100644 --- a/android/app/src/main/cpp/chiaki-jni.c +++ b/android/app/src/main/cpp/chiaki-jni.c @@ -702,6 +702,11 @@ JNIEXPORT void JNICALL JNI_FCN(registStart)(JNIEnv *env, jobject obj, jobject re ")V"); jclass regist_info_class = E->GetObjectClass(env, regist_info_obj); + + jobject target_obj = E->GetObjectField(env, regist_info_obj, E->GetFieldID(env, regist_info_class, "target", "L"BASE_PACKAGE"/Target;")); + jclass target_class = E->GetObjectClass(env, target_obj); + jint target_value = E->GetIntField(env, target_obj, E->GetFieldID(env, target_class, "value", "I")); + jstring host_string = E->GetObjectField(env, regist_info_obj, E->GetFieldID(env, regist_info_class, "host", "Ljava/lang/String;")); jboolean broadcast = E->GetBooleanField(env, regist_info_obj, E->GetFieldID(env, regist_info_class, "broadcast", "Z")); jstring psn_online_id_string = E->GetObjectField(env, regist_info_obj, E->GetFieldID(env, regist_info_class, "psnOnlineId", "Ljava/lang/String;")); @@ -709,6 +714,7 @@ JNIEXPORT void JNICALL JNI_FCN(registStart)(JNIEnv *env, jobject obj, jobject re jint pin = E->GetIntField(env, regist_info_obj, E->GetFieldID(env, regist_info_class, "pin", "I")); ChiakiRegistInfo regist_info = { 0 }; + regist_info.target = (ChiakiTarget)target_value; regist_info.host = E->GetStringUTFChars(env, host_string, NULL); regist_info.broadcast = broadcast; if(psn_online_id_string) diff --git a/android/app/src/main/java/com/metallic/chiaki/lib/Chiaki.kt b/android/app/src/main/java/com/metallic/chiaki/lib/Chiaki.kt index e511e22..bda96ac 100644 --- a/android/app/src/main/java/com/metallic/chiaki/lib/Chiaki.kt +++ b/android/app/src/main/java/com/metallic/chiaki/lib/Chiaki.kt @@ -3,11 +3,19 @@ package com.metallic.chiaki.lib import android.os.Parcelable import android.util.Log import android.view.Surface +import kotlinx.android.parcel.IgnoredOnParcel import kotlinx.android.parcel.Parcelize import java.lang.Exception import java.net.InetSocketAddress import kotlin.math.abs +enum class Target(val value: Int) +{ + PS4_8(800), + PS4_9(900), + PS4_10(1000) +} + enum class VideoResolutionPreset(val value: Int) { RES_360P(1), @@ -316,6 +324,7 @@ class DiscoveryService( @Parcelize data class RegistInfo( + val target: Target, val host: String, val broadcast: Boolean, val psnOnlineId: String?, diff --git a/android/app/src/main/java/com/metallic/chiaki/regist/RegistActivity.kt b/android/app/src/main/java/com/metallic/chiaki/regist/RegistActivity.kt index 04e29f0..6be03f9 100644 --- a/android/app/src/main/java/com/metallic/chiaki/regist/RegistActivity.kt +++ b/android/app/src/main/java/com/metallic/chiaki/regist/RegistActivity.kt @@ -28,6 +28,7 @@ import androidx.lifecycle.ViewModelProvider import com.metallic.chiaki.R import com.metallic.chiaki.common.ext.RevealActivity import com.metallic.chiaki.lib.RegistInfo +import com.metallic.chiaki.lib.Target import kotlinx.android.synthetic.main.activity_regist.* import java.lang.IllegalArgumentException @@ -63,7 +64,8 @@ class RegistActivity: AppCompatActivity(), RevealActivity registButton.setOnClickListener { doRegist() } - ps4VersionRadioGroup.check(when(viewModel.ps4Version.value ?: RegistViewModel.PS4Version.GE_7) { + ps4VersionRadioGroup.check(when(viewModel.ps4Version.value ?: RegistViewModel.PS4Version.GE_8) { + RegistViewModel.PS4Version.GE_8 -> R.id.ps4VersionGE8RadioButton RegistViewModel.PS4Version.GE_7 -> R.id.ps4VersionGE7RadioButton RegistViewModel.PS4Version.LT_7 -> R.id.ps4VersionLT7RadioButton }) @@ -71,6 +73,7 @@ class RegistActivity: AppCompatActivity(), RevealActivity ps4VersionRadioGroup.setOnCheckedChangeListener { _, checkedId -> viewModel.ps4Version.value = when(checkedId) { + R.id.ps4VersionGE8RadioButton -> RegistViewModel.PS4Version.GE_8 R.id.ps4VersionGE7RadioButton -> RegistViewModel.PS4Version.GE_7 R.id.ps4VersionLT7RadioButton -> RegistViewModel.PS4Version.LT_7 else -> RegistViewModel.PS4Version.GE_7 @@ -78,11 +81,11 @@ class RegistActivity: AppCompatActivity(), RevealActivity } viewModel.ps4Version.observe(this, Observer { - psnAccountIdHelpGroup.visibility = if(it == RegistViewModel.PS4Version.GE_7) View.VISIBLE else View.GONE + psnAccountIdHelpGroup.visibility = if(it == RegistViewModel.PS4Version.LT_7) View.GONE else View.VISIBLE psnIdTextInputLayout.hint = getString(when(it!!) { - RegistViewModel.PS4Version.GE_7 -> R.string.hint_regist_psn_account_id RegistViewModel.PS4Version.LT_7 -> R.string.hint_regist_psn_online_id + else -> R.string.hint_regist_psn_account_id }) }) } @@ -98,14 +101,14 @@ class RegistActivity: AppCompatActivity(), RevealActivity val psnId = psnIdEditText.text.toString().trim() val psnOnlineId: String? = if(ps4Version == RegistViewModel.PS4Version.LT_7) psnId else null val psnAccountId: ByteArray? = - if(ps4Version == RegistViewModel.PS4Version.GE_7) + if(ps4Version != RegistViewModel.PS4Version.LT_7) try { Base64.decode(psnId, Base64.DEFAULT) } catch(e: IllegalArgumentException) { null } else null val psnIdValid = when(ps4Version) { - RegistViewModel.PS4Version.GE_7 -> psnAccountId != null && psnAccountId.size == RegistInfo.ACCOUNT_ID_SIZE RegistViewModel.PS4Version.LT_7 -> psnOnlineId?.isNotEmpty() ?: false + else -> psnAccountId != null && psnAccountId.size == RegistInfo.ACCOUNT_ID_SIZE } @@ -117,8 +120,8 @@ class RegistActivity: AppCompatActivity(), RevealActivity if(!psnIdValid) getString(when(ps4Version) { - RegistViewModel.PS4Version.GE_7 -> R.string.regist_psn_account_id_invalid RegistViewModel.PS4Version.LT_7 -> R.string.regist_psn_online_id_invalid + else -> R.string.regist_psn_account_id_invalid }) else null @@ -127,7 +130,14 @@ class RegistActivity: AppCompatActivity(), RevealActivity if(!hostValid || !psnIdValid || !pinValid) return - val registInfo = RegistInfo(host, broadcast, psnOnlineId, psnAccountId, pin.toInt()) + val target = when(ps4Version) + { + RegistViewModel.PS4Version.GE_8 -> Target.PS4_10 + RegistViewModel.PS4Version.GE_7 -> Target.PS4_9 + RegistViewModel.PS4Version.LT_7 -> Target.PS4_8 + } + + val registInfo = RegistInfo(target, host, broadcast, psnOnlineId, psnAccountId, pin.toInt()) Intent(this, RegistExecuteActivity::class.java).also { it.putExtra(RegistExecuteActivity.EXTRA_REGIST_INFO, registInfo) diff --git a/android/app/src/main/java/com/metallic/chiaki/regist/RegistViewModel.kt b/android/app/src/main/java/com/metallic/chiaki/regist/RegistViewModel.kt index 873008e..bc14037 100644 --- a/android/app/src/main/java/com/metallic/chiaki/regist/RegistViewModel.kt +++ b/android/app/src/main/java/com/metallic/chiaki/regist/RegistViewModel.kt @@ -23,9 +23,10 @@ import androidx.lifecycle.ViewModel class RegistViewModel: ViewModel() { enum class PS4Version { + GE_8, GE_7, LT_7 } - val ps4Version = MutableLiveData(PS4Version.GE_7) + val ps4Version = MutableLiveData(PS4Version.GE_8) } \ No newline at end of file diff --git a/android/app/src/main/res/layout/activity_regist.xml b/android/app/src/main/res/layout/activity_regist.xml index fff5cad..e174bb0 100644 --- a/android/app/src/main/res/layout/activity_regist.xml +++ b/android/app/src/main/res/layout/activity_regist.xml @@ -69,13 +69,22 @@ android:orientation="horizontal" app:layout_constraintTop_toBottomOf="@id/broadcastCheckBox"> + + + android:checked="false" + android:text="@string/regist_option_ps4_ge_7" /> + Host Broadcast PS4 < 7.0 - PS4 ≥ 7.0 + PS4 ≥ 7.0, < 8 + PS4 ≥ 8.0 About obtaining your Account ID, see https://github.com/thestr4ng3r/chiaki/blob/master/README.md#obtaining-your-psn-accountid PSN Online ID (username, case-sensitive) diff --git a/gui/include/registdialog.h b/gui/include/registdialog.h index d159960..fc01ca2 100644 --- a/gui/include/registdialog.h +++ b/gui/include/registdialog.h @@ -41,9 +41,10 @@ class RegistDialog : public QDialog QLineEdit *host_edit; QCheckBox *broadcast_check_box; - QRadioButton *psn_online_id_radio_button; + QRadioButton *ps4_pre9_radio_button; + QRadioButton *ps4_pre10_radio_button; + QRadioButton *ps4_10_radio_button; QLineEdit *psn_online_id_edit; - QRadioButton *psn_account_id_radio_button; QLineEdit *psn_account_id_edit; QLineEdit *pin_edit; QDialogButtonBox *button_box; @@ -51,6 +52,8 @@ class RegistDialog : public QDialog RegisteredHost registered_host; + bool NeedAccountId(); + private slots: void ValidateInput(); diff --git a/gui/src/registdialog.cpp b/gui/src/registdialog.cpp index 07e0844..0adcbbb 100644 --- a/gui/src/registdialog.cpp +++ b/gui/src/registdialog.cpp @@ -59,20 +59,29 @@ RegistDialog::RegistDialog(Settings *settings, const QString &host, QWidget *par broadcast_check_box->setChecked(host.isEmpty()); auto UpdatePSNIDEdits = [this]() { - psn_online_id_edit->setEnabled(psn_online_id_radio_button->isChecked()); - psn_account_id_edit->setEnabled(psn_account_id_radio_button->isChecked()); + bool need_account_id = NeedAccountId(); + psn_online_id_edit->setEnabled(!need_account_id); + psn_account_id_edit->setEnabled(need_account_id); }; - psn_online_id_radio_button = new QRadioButton(tr("PSN Online-ID (username, case-sensitive) for PS4 < 7.0:"), this); - psn_online_id_edit = new QLineEdit(this); - form_layout->addRow(psn_online_id_radio_button, psn_online_id_edit); - connect(psn_online_id_radio_button, &QRadioButton::toggled, this, UpdatePSNIDEdits); + auto version_layout = new QVBoxLayout(nullptr); + ps4_pre9_radio_button = new QRadioButton(tr("< 7.0"), this); + version_layout->addWidget(ps4_pre9_radio_button); + connect(ps4_pre9_radio_button, &QRadioButton::toggled, this, UpdatePSNIDEdits); + ps4_pre10_radio_button = new QRadioButton(tr(">= 7.0, < 8.0"), this); + version_layout->addWidget(ps4_pre10_radio_button); + connect(ps4_pre10_radio_button, &QRadioButton::toggled, this, UpdatePSNIDEdits); + ps4_10_radio_button = new QRadioButton(tr(">= 8.0"), this); + version_layout->addWidget(ps4_10_radio_button); + connect(ps4_10_radio_button, &QRadioButton::toggled, this, UpdatePSNIDEdits); + form_layout->addRow(tr("PS4 Firmware:"), version_layout); - psn_account_id_radio_button = new QRadioButton(tr("PSN Account-ID (base64) for PS4 >= 7.0:"), this); + psn_online_id_edit = new QLineEdit(this); + form_layout->addRow(tr("PSN Online-ID (username, case-sensitive):"), psn_online_id_edit); psn_account_id_edit = new QLineEdit(this); - form_layout->addRow(psn_account_id_radio_button, psn_account_id_edit); - psn_account_id_radio_button->setChecked(true); - connect(psn_account_id_radio_button, &QRadioButton::toggled, this, UpdatePSNIDEdits); + form_layout->addRow(tr("PSN Account-ID (base64):"), psn_account_id_edit); + + ps4_10_radio_button->setChecked(true); UpdatePSNIDEdits(); @@ -96,11 +105,17 @@ RegistDialog::~RegistDialog() { } +bool RegistDialog::NeedAccountId() +{ + return !ps4_pre9_radio_button->isChecked(); +} + void RegistDialog::ValidateInput() { + bool need_account_id = NeedAccountId(); bool valid = !host_edit->text().trimmed().isEmpty() - && (!psn_online_id_radio_button->isChecked() || !psn_online_id_edit->text().trimmed().isEmpty()) - && (!psn_account_id_radio_button->isChecked() || !psn_account_id_radio_button->text().trimmed().isEmpty()) + && !(!need_account_id && psn_online_id_edit->text().trimmed().isEmpty()) + && !(need_account_id && psn_account_id_edit->text().trimmed().isEmpty()) && pin_edit->text().length() == PIN_LENGTH; register_button->setEnabled(valid); } @@ -111,8 +126,16 @@ void RegistDialog::accept() QByteArray host = host_edit->text().trimmed().toUtf8(); info.host = host.constData(); + if(ps4_pre9_radio_button->isChecked()) + info.target = CHIAKI_TARGET_PS4_8; + else if(ps4_pre10_radio_button->isChecked()) + info.target = CHIAKI_TARGET_PS4_9; + else + info.target = CHIAKI_TARGET_PS4_10; + + bool need_account_id = NeedAccountId(); QByteArray psn_id; // keep this out of the if scope - if(psn_online_id_radio_button->isChecked()) + if(!need_account_id) { psn_id = psn_online_id_edit->text().trimmed().toUtf8(); info.psn_online_id = psn_id.constData(); diff --git a/lib/include/chiaki/common.h b/lib/include/chiaki/common.h index a2e87a4..484604e 100644 --- a/lib/include/chiaki/common.h +++ b/lib/include/chiaki/common.h @@ -72,6 +72,15 @@ CHIAKI_EXPORT const char *chiaki_error_string(ChiakiErrorCode code); CHIAKI_EXPORT void *chiaki_aligned_alloc(size_t alignment, size_t size); CHIAKI_EXPORT void chiaki_aligned_free(void *ptr); +typedef enum +{ + // values must not change + CHIAKI_TARGET_PS4_UNKNOWN = 0, + CHIAKI_TARGET_PS4_8 = 800, + CHIAKI_TARGET_PS4_9 = 900, + CHIAKI_TARGET_PS4_10 = 1000 +} ChiakiTarget; + /** * Perform initialization of global state needed for using the Chiaki lib */ diff --git a/lib/include/chiaki/regist.h b/lib/include/chiaki/regist.h index 6140967..d78b1ef 100644 --- a/lib/include/chiaki/regist.h +++ b/lib/include/chiaki/regist.h @@ -33,6 +33,7 @@ extern "C" { typedef struct chiaki_regist_info_t { + ChiakiTarget target; const char *host; bool broadcast; @@ -94,7 +95,7 @@ CHIAKI_EXPORT void chiaki_regist_stop(ChiakiRegist *regist); /** * @param psn_account_id must be exactly of size CHIAKI_PSN_ACCOUNT_ID_SIZE */ -CHIAKI_EXPORT ChiakiErrorCode chiaki_regist_request_payload_format(uint8_t *buf, size_t *buf_size, ChiakiRPCrypt *crypt, const char *psn_online_id, const uint8_t *psn_account_id); +CHIAKI_EXPORT ChiakiErrorCode chiaki_regist_request_payload_format(ChiakiTarget target, const uint8_t *ambassador, uint8_t *buf, size_t *buf_size, ChiakiRPCrypt *crypt, const char *psn_online_id, const uint8_t *psn_account_id, uint32_t pin); #ifdef __cplusplus } diff --git a/lib/include/chiaki/rpcrypt.h b/lib/include/chiaki/rpcrypt.h index 12d0705..ef30abc 100644 --- a/lib/include/chiaki/rpcrypt.h +++ b/lib/include/chiaki/rpcrypt.h @@ -31,15 +31,18 @@ extern "C" { typedef struct chiaki_rpcrypt_t { + ChiakiTarget target; uint8_t bright[CHIAKI_RPCRYPT_KEY_SIZE]; uint8_t ambassador[CHIAKI_RPCRYPT_KEY_SIZE]; } ChiakiRPCrypt; -CHIAKI_EXPORT void chiaki_rpcrypt_bright_ambassador(uint8_t *bright, uint8_t *ambassador, const uint8_t *nonce, const uint8_t *morning); -CHIAKI_EXPORT void chiaki_rpcrypt_aeropause(uint8_t *aeropause, const uint8_t *ambassador); +CHIAKI_EXPORT void chiaki_rpcrypt_bright_ambassador(ChiakiTarget target, uint8_t *bright, uint8_t *ambassador, const uint8_t *nonce, const uint8_t *morning); +CHIAKI_EXPORT void chiaki_rpcrypt_aeropause_ps4_pre10(uint8_t *aeropause, const uint8_t *ambassador); +CHIAKI_EXPORT void chiaki_rpcrypt_aeropause(size_t key_1_off, uint8_t *aeropause, const uint8_t *ambassador); -CHIAKI_EXPORT void chiaki_rpcrypt_init_auth(ChiakiRPCrypt *rpcrypt, const uint8_t *nonce, const uint8_t *morning); -CHIAKI_EXPORT void chiaki_rpcrypt_init_regist(ChiakiRPCrypt *rpcrypt, const uint8_t *ambassador, uint32_t pin); +CHIAKI_EXPORT void chiaki_rpcrypt_init_auth(ChiakiRPCrypt *rpcrypt, ChiakiTarget target, const uint8_t *nonce, const uint8_t *morning); +CHIAKI_EXPORT void chiaki_rpcrypt_init_regist_ps4_pre10(ChiakiRPCrypt *rpcrypt, const uint8_t *ambassador, uint32_t pin); +CHIAKI_EXPORT ChiakiErrorCode chiaki_rpcrypt_init_regist(ChiakiRPCrypt *rpcrypt, const uint8_t *ambassador, size_t key_0_off, uint32_t pin); CHIAKI_EXPORT ChiakiErrorCode chiaki_rpcrypt_generate_iv(ChiakiRPCrypt *rpcrypt, uint8_t *iv, uint64_t counter); CHIAKI_EXPORT ChiakiErrorCode chiaki_rpcrypt_encrypt(ChiakiRPCrypt *rpcrypt, uint64_t counter, const uint8_t *in, uint8_t *out, size_t sz); CHIAKI_EXPORT ChiakiErrorCode chiaki_rpcrypt_decrypt(ChiakiRPCrypt *rpcrypt, uint64_t counter, const uint8_t *in, uint8_t *out, size_t sz); diff --git a/lib/include/chiaki/session.h b/lib/include/chiaki/session.h index a18ccfb..81d5fe3 100644 --- a/lib/include/chiaki/session.h +++ b/lib/include/chiaki/session.h @@ -43,22 +43,16 @@ extern "C" { #define CHIAKI_RP_APPLICATION_REASON_IN_USE 0x80108b10 #define CHIAKI_RP_APPLICATION_REASON_CRASH 0x80108b15 #define CHIAKI_RP_APPLICATION_REASON_RP_VERSION 0x80108b11 -// unknown: 0x80108bff +#define CHIAKI_RP_APPLICATION_REASON_UNKNOWN 0x80108bff CHIAKI_EXPORT const char *chiaki_rp_application_reason_string(uint32_t reason); -typedef enum { - CHIAKI_RP_VERSION_UNKNOWN = 0, - CHIAKI_RP_VERSION_8_0 = 800, - CHIAKI_RP_VERSION_9_0 = 900 -} ChiakiRpVersion; - /** * @return RP-Version string or NULL */ -CHIAKI_EXPORT const char *chiaki_rp_version_string(ChiakiRpVersion version); +CHIAKI_EXPORT const char *chiaki_rp_version_string(ChiakiTarget target); -CHIAKI_EXPORT ChiakiRpVersion chiaki_rp_version_parse(const char *rp_version_str); +CHIAKI_EXPORT ChiakiTarget chiaki_rp_version_parse(const char *rp_version_str); #define CHIAKI_RP_DID_SIZE 32 @@ -171,7 +165,7 @@ typedef struct chiaki_session_t ChiakiConnectVideoProfile video_profile; } connect_info; - ChiakiRpVersion rp_version; + ChiakiTarget target; uint8_t nonce[CHIAKI_RPCRYPT_KEY_SIZE]; ChiakiRPCrypt rpcrypt; diff --git a/lib/src/ctrl.c b/lib/src/ctrl.c index 3304344..bca281f 100644 --- a/lib/src/ctrl.c +++ b/lib/src/ctrl.c @@ -576,9 +576,23 @@ static ChiakiErrorCode ctrl_connect(ChiakiCtrl *ctrl) if(err != CHIAKI_ERR_SUCCESS) goto error; + char bitrate_b64[256]; + bool have_bitrate = session->target >= CHIAKI_TARGET_PS4_10; + if(have_bitrate) + { + uint8_t bitrate[4] = { 0 }; + uint8_t bitrate_enc[4] = { 0 }; + err = chiaki_rpcrypt_encrypt(&session->rpcrypt, ctrl->crypt_counter_local++, (const uint8_t *)bitrate, bitrate_enc, 4); + if(err != CHIAKI_ERR_SUCCESS) + goto error; + + err = chiaki_base64_encode(bitrate_enc, 4, bitrate_b64, sizeof(bitrate_b64)); + if(err != CHIAKI_ERR_SUCCESS) + goto error; + } static const char request_fmt[] = - "GET /sce/rp/session/ctrl HTTP/1.1\r\n" + "GET %s HTTP/1.1\r\n" "Host: %s:%d\r\n" "User-Agent: remoteplay Windows\r\n" "Connection: keep-alive\r\n" @@ -589,17 +603,27 @@ static ChiakiErrorCode ctrl_connect(ChiakiCtrl *ctrl) "RP-ControllerType: 3\r\n" "RP-ClientType: 11\r\n" "RP-OSType: %s\r\n" - "RP-ConPath: 1\r\n\r\n"; + "RP-ConPath: 1\r\n" + "%s%s%s" + "\r\n"; - const char *rp_version = chiaki_rp_version_string(session->rp_version); + const char *path = (session->target == CHIAKI_TARGET_PS4_8 || session->target == CHIAKI_TARGET_PS4_9) + ? "/sce/rp/session/ctrl" + : "/sie/ps4/rp/sess/ctrl"; + const char *rp_version = chiaki_rp_version_string(session->target); char buf[512]; int request_len = snprintf(buf, sizeof(buf), request_fmt, - session->connect_info.hostname, SESSION_CTRL_PORT, auth_b64, rp_version ? rp_version : "", did_b64, ostype_b64); + path, session->connect_info.hostname, SESSION_CTRL_PORT, auth_b64, + rp_version ? rp_version : "", did_b64, ostype_b64, + have_bitrate ? "RP-StartBitrate: " : "", + have_bitrate ? bitrate_b64 : "", + have_bitrate ? "\r\n" : ""); if(request_len < 0 || request_len >= sizeof(buf)) goto error; CHIAKI_LOGI(session->log, "Sending ctrl request"); + chiaki_log_hexdump(session->log, CHIAKI_LOG_VERBOSE, (const uint8_t *)buf, (size_t)request_len); int sent = send(sock, buf, (size_t)request_len, 0); if(sent < 0) diff --git a/lib/src/regist.c b/lib/src/regist.c index a50504e..6cc6e18 100644 --- a/lib/src/regist.c +++ b/lib/src/regist.c @@ -106,34 +106,42 @@ static void regist_event_simple(ChiakiRegist *regist, ChiakiRegistEventType type regist->cb(&event, regist->cb_user); } -static const char * const request_head_fmt = - "POST /sce/rp/regist HTTP/1.1\r\n" +static const char *const request_head_fmt = + "POST %s HTTP/1.1\r\n HTTP/1.1\r\n" "HOST: 10.0.2.15\r\n" // random lol "User-Agent: remoteplay Windows\r\n" "Connection: close\r\n" "Content-Length: %llu\r\n"; -static const char * const request_rp_version_fmt = "RP-Version: %s\r\n"; +static const char *request_path = "/sie/ps4/rp/sess/rgst"; +static const char *request_path_ps4_pre10 = "/sce/rp/regist"; -static const char * const request_tail = "\r\n"; +static const char *const request_rp_version_fmt = "RP-Version: %s\r\n"; -static const char * const request_inner_account_id_fmt = - "Client-Type: Windows\r\n" +static const char *const request_tail = "\r\n"; + +const char *client_type = "dabfa2ec873de5839bee8d3f4c0239c4282c07c25c6077a2931afcf0adc0d34f"; +const char *client_type_ps4_pre10 = "Windows"; + +static const char *const request_inner_account_id_fmt = + "Client-Type: %s\r\n" "Np-AccountId: %s\r\n"; -static const char * const request_inner_online_id_fmt = +static const char *const request_inner_online_id_fmt = "Client-Type: Windows\r\n" "Np-Online-Id: %s\r\n"; -static int request_header_format(char *buf, size_t buf_size, size_t payload_size, ChiakiRpVersion rp_version) +static int request_header_format(char *buf, size_t buf_size, size_t payload_size, ChiakiTarget target) { - int cur = snprintf(buf, buf_size, request_head_fmt, (unsigned long long)payload_size); + int cur = snprintf(buf, buf_size, request_head_fmt, + target < CHIAKI_TARGET_PS4_10 ? request_path_ps4_pre10 : request_path, + (unsigned long long)payload_size); if(cur < 0 || cur >= payload_size) return -1; - if(rp_version >= CHIAKI_RP_VERSION_9_0) + if(target >= CHIAKI_TARGET_PS4_9) { - const char *rp_version_str = chiaki_rp_version_string(rp_version); + const char *rp_version_str = chiaki_rp_version_string(target); size_t s = buf_size - cur; int r = snprintf(buf + cur, s, request_rp_version_fmt, rp_version_str); if(r < 0 || r >= s) @@ -149,14 +157,31 @@ static int request_header_format(char *buf, size_t buf_size, size_t payload_size } -CHIAKI_EXPORT ChiakiErrorCode chiaki_regist_request_payload_format(uint8_t *buf, size_t *buf_size, ChiakiRPCrypt *crypt, const char *psn_online_id, const uint8_t *psn_account_id) +CHIAKI_EXPORT ChiakiErrorCode chiaki_regist_request_payload_format(ChiakiTarget target, const uint8_t *ambassador, uint8_t *buf, size_t *buf_size, ChiakiRPCrypt *crypt, const char *psn_online_id, const uint8_t *psn_account_id, uint32_t pin) { size_t buf_size_val = *buf_size; static const size_t inner_header_off = 0x1e0; if(buf_size_val < inner_header_off) return CHIAKI_ERR_BUF_TOO_SMALL; - memset(buf, 'A', inner_header_off); - chiaki_rpcrypt_aeropause(buf + 0x11c, crypt->ambassador); + memset(buf, 'A', inner_header_off); // can be random + + if(target < CHIAKI_TARGET_PS4_10) + { + chiaki_rpcrypt_init_regist_ps4_pre10(crypt, ambassador, pin); + chiaki_rpcrypt_aeropause_ps4_pre10(buf + 0x11c, crypt->ambassador); + } + else + { + size_t key_0_off = buf[0x18D] & 0x1F; + size_t key_1_off = buf[0] >> 3; + chiaki_rpcrypt_init_regist(crypt, ambassador, key_0_off, pin); + uint8_t aeropause[0x10]; + chiaki_rpcrypt_aeropause(key_1_off, aeropause, crypt->ambassador); + memcpy(buf + 0xc7, aeropause + 8, 8); + memcpy(buf + 0x191, aeropause, 8); + psn_online_id = NULL; // don't need this + } + int inner_header_size; if(psn_online_id) inner_header_size = snprintf((char *)buf + inner_header_off, buf_size_val - inner_header_off, request_inner_online_id_fmt, psn_online_id); @@ -166,7 +191,10 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_regist_request_payload_format(uint8_t *buf, ChiakiErrorCode err = chiaki_base64_encode(psn_account_id, CHIAKI_PSN_ACCOUNT_ID_SIZE, account_id_b64, sizeof(account_id_b64)); if(err != CHIAKI_ERR_SUCCESS) return err; - inner_header_size = snprintf((char *)buf + inner_header_off, buf_size_val - inner_header_off, request_inner_account_id_fmt, account_id_b64); + inner_header_size = snprintf((char *)buf + inner_header_off, buf_size_val - inner_header_off, + request_inner_account_id_fmt, + target < CHIAKI_TARGET_PS4_10 ? client_type_ps4_pre10 : client_type, + account_id_b64); } else return CHIAKI_ERR_INVALID_DATA; @@ -184,29 +212,26 @@ static void *regist_thread_func(void *user) bool canceled = false; bool success = false; + ChiakiRPCrypt crypt; uint8_t ambassador[CHIAKI_RPCRYPT_KEY_SIZE]; ChiakiErrorCode err = chiaki_random_bytes_crypt(ambassador, sizeof(ambassador)); if(err != CHIAKI_ERR_SUCCESS) { - CHIAKI_LOGE(regist->log, "Regist failed to generate random nonce"); + CHIAKI_LOGE(regist->log, "Regist failed to generate random ambassador"); goto fail; } - ChiakiRPCrypt crypt; - chiaki_rpcrypt_init_regist(&crypt, ambassador, regist->info.pin); - uint8_t payload[0x400]; size_t payload_size = sizeof(payload); - err = chiaki_regist_request_payload_format(payload, &payload_size, &crypt, regist->info.psn_online_id, regist->info.psn_account_id); + err = chiaki_regist_request_payload_format(regist->info.target, ambassador, payload, &payload_size, &crypt, regist->info.psn_online_id, regist->info.psn_account_id, regist->info.pin); if(err != CHIAKI_ERR_SUCCESS) { CHIAKI_LOGE(regist->log, "Regist failed to format payload"); goto fail; } - ChiakiRpVersion rp_version = regist->info.psn_online_id ? CHIAKI_RP_VERSION_8_0 : CHIAKI_RP_VERSION_9_0; char request_header[0x100]; - int request_header_size = request_header_format(request_header, sizeof(request_header), payload_size, rp_version); + int request_header_size = request_header_format(request_header, sizeof(request_header), payload_size, regist->info.target); if(request_header_size < 0 || request_header_size >= sizeof(request_header)) { diff --git a/lib/src/rpcrypt.c b/lib/src/rpcrypt.c index 5659105..a791d6b 100644 --- a/lib/src/rpcrypt.c +++ b/lib/src/rpcrypt.c @@ -31,7 +31,7 @@ static const uint8_t echo_b[] = { 0xe1, 0xec, 0x9c, 0x3a, 0xdd, 0xbd, 0x08, 0x85, 0xfc, 0x0e, 0x1d, 0x78, 0x90, 0x32, 0xc0, 0x04 }; -CHIAKI_EXPORT void chiaki_rpcrypt_bright_ambassador(uint8_t *bright, uint8_t *ambassador, const uint8_t *nonce, const uint8_t *morning) +static void bright_ambassador_ps4_pre10(uint8_t *bright, uint8_t *ambassador, const uint8_t *nonce, const uint8_t *morning) { static const uint8_t echo_a[] = { 0x01, 0x49, 0x87, 0x9b, 0x65, 0x39, 0x8b, 0x39, 0x4b, 0x3a, 0x8d, 0x48, 0xc3, 0x0a, 0xef, 0x51 }; @@ -55,7 +55,768 @@ CHIAKI_EXPORT void chiaki_rpcrypt_bright_ambassador(uint8_t *bright, uint8_t *am } } -CHIAKI_EXPORT void chiaki_rpcrypt_aeropause(uint8_t *aeropause, const uint8_t *ambassador) +static void bright_ambassador(uint8_t *bright, uint8_t *ambassador, const uint8_t *nonce, const uint8_t *morning) +{ + static const uint8_t static_keys_a[0x70 * 0x20] = { + 0xdf, 0x40, 0x82, 0xa6, 0x2e, 0xd8, 0x6b, 0x2b, 0xf5, 0x6c, + 0x5f, 0xf5, 0xfb, 0x21, 0x69, 0xb4, 0x00, 0x27, 0x5e, 0x87, + 0xfa, 0x1a, 0xa7, 0x95, 0x16, 0xa1, 0xb7, 0xb7, 0xca, 0xf2, + 0x3d, 0x7a, 0x04, 0x97, 0xef, 0x49, 0xeb, 0x51, 0xbc, 0xe9, + 0x2a, 0x47, 0x84, 0xba, 0xfb, 0x08, 0x78, 0x4a, 0xa7, 0x82, + 0x17, 0xad, 0x4b, 0x26, 0x34, 0x97, 0xb2, 0xde, 0x41, 0x7d, + 0xc6, 0x08, 0xc4, 0xe9, 0xe7, 0x73, 0xa1, 0x22, 0xa2, 0xf3, + 0x2a, 0x6f, 0x8c, 0xc0, 0x45, 0xf6, 0x84, 0xb4, 0xd7, 0x54, + 0x07, 0xd5, 0x76, 0x33, 0xd3, 0x8b, 0x61, 0xe7, 0xea, 0xd6, + 0x98, 0x32, 0xf5, 0xeb, 0x9e, 0x1b, 0xbd, 0xe2, 0x01, 0x31, + 0x45, 0x01, 0x0d, 0x51, 0x1f, 0x77, 0xaf, 0x0c, 0x34, 0xb7, + 0x23, 0x40, 0x0d, 0xc0, 0xac, 0x7e, 0x04, 0xb5, 0xf0, 0x75, + 0xde, 0x5e, 0xba, 0xfa, 0xa6, 0x25, 0x56, 0xde, 0x53, 0x83, + 0x94, 0x4e, 0xa4, 0xd9, 0x4e, 0xf4, 0x73, 0xab, 0xd6, 0x96, + 0xb6, 0xfc, 0x09, 0x46, 0xe2, 0x1a, 0x9d, 0x4f, 0xf1, 0x89, + 0x90, 0xa0, 0x34, 0xf4, 0x55, 0x60, 0x29, 0xf9, 0x39, 0xdb, + 0x20, 0xaa, 0xdf, 0x8b, 0xff, 0x6c, 0xc0, 0xa9, 0x96, 0xad, + 0x71, 0x67, 0xae, 0xb6, 0x1a, 0x6e, 0xd4, 0x92, 0x7a, 0xc7, + 0xac, 0x31, 0xc8, 0x21, 0x24, 0x3b, 0xb3, 0x6a, 0x1d, 0x4d, + 0xc0, 0x82, 0x5f, 0x53, 0x9f, 0xf9, 0xeb, 0x4a, 0x41, 0x47, + 0x27, 0x5a, 0xbc, 0x3b, 0xc8, 0x50, 0x20, 0xeb, 0x11, 0x02, + 0xf8, 0xc5, 0x7e, 0xa4, 0xd3, 0x85, 0x52, 0x90, 0x61, 0x4e, + 0x88, 0xff, 0x81, 0xcf, 0x75, 0x58, 0x21, 0xfe, 0x58, 0x8f, + 0xd0, 0x75, 0xf6, 0x20, 0xaa, 0x54, 0x3a, 0x45, 0x54, 0xf3, + 0xe4, 0x44, 0xda, 0x98, 0x7f, 0x09, 0x9e, 0xa6, 0x72, 0x63, + 0xbc, 0x9f, 0x46, 0x77, 0x7e, 0x24, 0xf8, 0xda, 0xc4, 0x94, + 0x04, 0xa9, 0x23, 0xa7, 0xea, 0x67, 0xe3, 0x85, 0x41, 0xa4, + 0x4b, 0x8e, 0xc1, 0x92, 0xde, 0x96, 0xc8, 0x09, 0xb7, 0xd4, + 0x41, 0x60, 0x89, 0xf0, 0xdb, 0x09, 0x56, 0x01, 0xc1, 0x6c, + 0x33, 0xb1, 0x37, 0xbd, 0x79, 0x6f, 0xab, 0x1c, 0x61, 0xe9, + 0x18, 0xfc, 0xb3, 0x6c, 0xbc, 0xfe, 0xea, 0xb1, 0x9e, 0xce, + 0xba, 0x83, 0xda, 0xe5, 0x81, 0xd7, 0x07, 0xb7, 0x8a, 0xf0, + 0x89, 0x39, 0xdc, 0x30, 0x6b, 0x0d, 0xa7, 0x2b, 0xc9, 0x51, + 0xdc, 0x4f, 0x99, 0x98, 0x3c, 0xcf, 0x62, 0x1c, 0x56, 0xf1, + 0x86, 0x88, 0xdb, 0xce, 0x10, 0xbf, 0x3d, 0xde, 0xe6, 0xf5, + 0x02, 0x51, 0xf7, 0xa5, 0xe3, 0x0a, 0xdb, 0xeb, 0xae, 0xb0, + 0x76, 0x89, 0x62, 0x4b, 0xa7, 0xbd, 0xd6, 0xfe, 0x8d, 0x01, + 0x28, 0x26, 0xf3, 0x76, 0x65, 0xba, 0x0f, 0x44, 0x77, 0x65, + 0x79, 0x47, 0x06, 0x43, 0xae, 0xa8, 0xd1, 0x5e, 0x20, 0x0e, + 0x72, 0x2a, 0x15, 0xd9, 0x42, 0x96, 0xe7, 0xdb, 0x40, 0xae, + 0x4e, 0x2b, 0x1c, 0x6d, 0x71, 0xa2, 0x8e, 0xa6, 0xe1, 0x4d, + 0xe8, 0x84, 0xa3, 0xa5, 0x59, 0x41, 0x0c, 0x9e, 0xe9, 0x3e, + 0x5a, 0xc8, 0x02, 0x3f, 0xbb, 0xf3, 0xe1, 0x0a, 0xf7, 0x3d, + 0xd7, 0xaa, 0xe5, 0x64, 0xc6, 0x4e, 0xc3, 0xc9, 0xbb, 0x32, + 0x30, 0xa9, 0x18, 0xec, 0xb8, 0xae, 0x61, 0xc9, 0x1a, 0x62, + 0xeb, 0x47, 0x92, 0x0c, 0xa7, 0xe0, 0x6c, 0x33, 0xf5, 0x84, + 0xec, 0x69, 0x31, 0xdb, 0xad, 0xdf, 0x3d, 0xbb, 0x4c, 0x4b, + 0xa1, 0x8c, 0xf0, 0x46, 0x52, 0x65, 0x72, 0xaf, 0x6a, 0xc8, + 0xce, 0xb3, 0x27, 0xd3, 0x32, 0x78, 0xb1, 0x39, 0xd6, 0xd1, + 0x5e, 0x09, 0x06, 0xff, 0xdc, 0x8f, 0xca, 0x16, 0x9b, 0x5e, + 0xa2, 0x18, 0x64, 0xee, 0x8c, 0x0b, 0x9b, 0xd7, 0x41, 0xf8, + 0x35, 0x98, 0xd3, 0x5c, 0x78, 0xe2, 0xbb, 0xa5, 0xf7, 0x68, + 0x02, 0x8a, 0xd2, 0x14, 0x85, 0x50, 0x8b, 0x98, 0x2a, 0x09, + 0x15, 0xc0, 0x9e, 0x9c, 0x47, 0x1f, 0x92, 0xc2, 0xa5, 0x2a, + 0x3b, 0xaa, 0x7b, 0xde, 0xb8, 0xe7, 0xee, 0xa7, 0xe0, 0x4a, + 0xb4, 0x81, 0x28, 0x7f, 0x15, 0x38, 0xe8, 0x78, 0xff, 0xc1, + 0xed, 0x98, 0xd8, 0x4f, 0x23, 0x3f, 0x77, 0x29, 0xac, 0xa5, + 0xbc, 0xd0, 0x8f, 0x2b, 0x89, 0xb9, 0x99, 0xbe, 0xc1, 0xf1, + 0x3e, 0x4e, 0xbd, 0x30, 0xd7, 0x6c, 0x1c, 0x09, 0x4e, 0x7c, + 0x13, 0x28, 0xf5, 0xc3, 0xff, 0xc1, 0xcf, 0x8a, 0x2a, 0x41, + 0xd0, 0x73, 0x24, 0x23, 0x31, 0x13, 0x77, 0xa4, 0x33, 0xc0, + 0x7e, 0x7b, 0xc0, 0x81, 0xb5, 0xf5, 0x25, 0x80, 0x45, 0xc5, + 0x94, 0x65, 0xa4, 0x8a, 0x94, 0xc1, 0xb6, 0xf4, 0x69, 0x61, + 0x11, 0xd5, 0x43, 0xea, 0x4b, 0x38, 0x4a, 0x56, 0x14, 0xdd, + 0x3b, 0xa6, 0x99, 0xdf, 0xe5, 0x87, 0x2a, 0xf9, 0x12, 0xbf, + 0x45, 0xb1, 0xd9, 0x74, 0x67, 0x8b, 0x2b, 0x57, 0xc3, 0xd0, + 0xb1, 0x9a, 0x4a, 0xcc, 0x4f, 0x63, 0x79, 0x5a, 0x4f, 0x1d, + 0xa6, 0xff, 0x65, 0xee, 0xbe, 0xd9, 0x16, 0xea, 0xa9, 0x66, + 0x07, 0x97, 0x71, 0x43, 0xdd, 0x64, 0xe3, 0x00, 0x03, 0xbc, + 0xba, 0x1b, 0xf3, 0xa6, 0xa6, 0xee, 0xef, 0xf8, 0xa7, 0xf2, + 0x15, 0xf9, 0xcf, 0x8c, 0x62, 0x2a, 0x1e, 0x9b, 0xbe, 0xfb, + 0x5a, 0xb7, 0xed, 0xd5, 0xd2, 0x71, 0x2a, 0xc9, 0xeb, 0x65, + 0x68, 0x77, 0x34, 0xa4, 0xbb, 0x44, 0xab, 0x01, 0xe5, 0x79, + 0x19, 0x37, 0xb2, 0x07, 0x01, 0xc6, 0xca, 0xb3, 0x07, 0xb8, + 0x55, 0xac, 0x03, 0xae, 0xf6, 0x3f, 0x29, 0x12, 0xa1, 0xba, + 0x1d, 0x94, 0x3a, 0xa3, 0xb5, 0x6c, 0x6e, 0xf4, 0x31, 0x3f, + 0x38, 0xa7, 0xe0, 0x34, 0x31, 0xf2, 0x2a, 0x7a, 0xe6, 0xcf, + 0x3f, 0xc4, 0xe9, 0xb9, 0x0f, 0x05, 0x9f, 0xec, 0x9a, 0xdc, + 0xb3, 0x59, 0xc4, 0x61, 0xaa, 0x10, 0xb6, 0xce, 0x73, 0xe3, + 0xe3, 0x8c, 0xf8, 0x5d, 0x0e, 0x95, 0x3e, 0x1a, 0x4f, 0xbe, + 0x77, 0x87, 0xce, 0x8a, 0x17, 0xf5, 0x20, 0xa7, 0x01, 0xf0, + 0xd7, 0xe6, 0x34, 0x63, 0x04, 0x76, 0xbe, 0x74, 0xb3, 0x44, + 0x7d, 0xdf, 0x11, 0x32, 0xb9, 0x13, 0x32, 0x15, 0x1a, 0xed, + 0xcf, 0x1f, 0x85, 0x91, 0xdb, 0xa3, 0xc4, 0xf2, 0x0f, 0x2c, + 0x8f, 0x77, 0x0a, 0xc4, 0x09, 0x7d, 0x19, 0x12, 0x7c, 0x46, + 0xf7, 0x9d, 0x3a, 0x94, 0x86, 0x4b, 0x22, 0x67, 0x6f, 0x90, + 0x57, 0x99, 0x70, 0x28, 0x3d, 0x9c, 0x53, 0xd2, 0xd3, 0x0b, + 0xc1, 0x29, 0xb7, 0x39, 0x8c, 0xb7, 0x62, 0xc8, 0xbb, 0x64, + 0x4d, 0xcf, 0xe0, 0x5f, 0x64, 0xfc, 0x47, 0xb2, 0xef, 0x0a, + 0x68, 0x8d, 0x9a, 0xcd, 0x9f, 0x49, 0x77, 0x6b, 0x50, 0x47, + 0x7c, 0xfc, 0xa4, 0xd4, 0x2c, 0x4d, 0x53, 0xc1, 0x26, 0x26, + 0x92, 0x38, 0x0d, 0xba, 0x1c, 0x71, 0x81, 0x17, 0x4a, 0x59, + 0x0f, 0x80, 0xfd, 0xad, 0x69, 0x5c, 0x58, 0xa7, 0xff, 0xef, + 0x1a, 0xca, 0xd6, 0xfb, 0x7e, 0x09, 0xdc, 0x32, 0x17, 0x34, + 0x31, 0x9c, 0xfa, 0x78, 0xf9, 0x88, 0x1a, 0x50, 0x6b, 0x09, + 0x24, 0xfe, 0x75, 0xcd, 0x22, 0xbd, 0xf0, 0x12, 0x10, 0x9d, + 0xc5, 0x7b, 0xbe, 0x9f, 0xa4, 0xc8, 0x7a, 0x7c, 0xb1, 0xa7, + 0x46, 0x2d, 0x48, 0xa1, 0x08, 0xd8, 0x75, 0x3c, 0x40, 0x53, + 0x59, 0x5a, 0x04, 0xf6, 0xcb, 0xd6, 0x2f, 0x51, 0x9e, 0x4a, + 0xa8, 0xef, 0x02, 0x3c, 0x61, 0xa8, 0x45, 0x5a, 0x99, 0x9a, + 0x73, 0x09, 0xc0, 0x71, 0x6f, 0x4d, 0xb7, 0x2c, 0x69, 0x6c, + 0x1e, 0x85, 0x48, 0xea, 0x33, 0x5b, 0xb1, 0x28, 0x09, 0xe3, + 0x24, 0x47, 0x7a, 0x12, 0xd8, 0x77, 0xa0, 0xfd, 0x3c, 0xcd, + 0x23, 0x53, 0x61, 0xf0, 0x9e, 0x49, 0x44, 0x19, 0x9c, 0xe1, + 0x0a, 0xb6, 0xc5, 0x93, 0x35, 0x4d, 0x06, 0xde, 0xb5, 0xac, + 0x15, 0xe2, 0x8d, 0x47, 0x6a, 0x8a, 0x29, 0x58, 0x8f, 0xfc, + 0x2b, 0x32, 0xc4, 0x0e, 0x4e, 0x75, 0xb2, 0x71, 0x44, 0x91, + 0xb6, 0xfb, 0x50, 0x60, 0xc4, 0x50, 0x4d, 0xd9, 0x1c, 0xd7, + 0x28, 0xb5, 0x2e, 0x08, 0x82, 0xfd, 0xbd, 0x7c, 0x48, 0x62, + 0x00, 0x01, 0x30, 0xa5, 0x47, 0x2a, 0x10, 0x4b, 0xfe, 0x3e, + 0xb5, 0xd7, 0xe2, 0x1e, 0x5a, 0xd0, 0xe3, 0x6b, 0xa0, 0x9c, + 0xe3, 0x12, 0x05, 0xb9, 0x1c, 0x7b, 0x3f, 0x18, 0x88, 0x11, + 0xc3, 0x4d, 0xe8, 0x83, 0x78, 0xf1, 0x8c, 0x6a, 0xb3, 0x87, + 0x7f, 0x67, 0xdf, 0x47, 0xc4, 0x2f, 0xea, 0x9c, 0x79, 0x98, + 0x35, 0x2d, 0x7b, 0x2d, 0xc7, 0x3e, 0x31, 0x13, 0x6e, 0xf3, + 0xfd, 0xa8, 0x16, 0x25, 0x03, 0x3f, 0xb1, 0x14, 0x77, 0xff, + 0xa1, 0xf2, 0xe9, 0x98, 0x32, 0xe4, 0x2f, 0x3e, 0xb0, 0x6e, + 0x29, 0x84, 0x0f, 0xdd, 0x85, 0xbb, 0xda, 0x0a, 0xc6, 0x19, + 0x78, 0x9a, 0x6b, 0xdd, 0x8b, 0x40, 0x39, 0x1f, 0x19, 0xce, + 0xad, 0x79, 0xc9, 0xda, 0x59, 0xbf, 0x6e, 0xbc, 0xb0, 0xc8, + 0x6e, 0x61, 0xeb, 0x2e, 0xba, 0xa6, 0x50, 0x06, 0x8c, 0x74, + 0xab, 0x23, 0x4f, 0x9d, 0x9e, 0x20, 0x67, 0x9a, 0x0c, 0xea, + 0x60, 0x14, 0x85, 0x42, 0xc4, 0x7d, 0xc0, 0x1c, 0x62, 0xa5, + 0x7e, 0x65, 0x1d, 0xee, 0xa1, 0xfd, 0xed, 0x82, 0x19, 0x1e, + 0x90, 0x87, 0xab, 0xdb, 0x80, 0xb7, 0xb2, 0xab, 0xa0, 0xbc, + 0xdb, 0x99, 0x9b, 0x2e, 0x8e, 0xc4, 0x96, 0xfe, 0x31, 0x82, + 0x9e, 0xe9, 0xc9, 0xa3, 0xd0, 0xe3, 0x52, 0x0f, 0x73, 0x24, + 0xd7, 0xb6, 0x41, 0xcc, 0xfc, 0xae, 0x86, 0xb5, 0x79, 0xe5, + 0x25, 0xc8, 0x76, 0x60, 0x48, 0x3c, 0xa4, 0x74, 0x6a, 0x7b, + 0x7e, 0xa8, 0x5b, 0xf8, 0x5b, 0x79, 0x0f, 0x19, 0x95, 0x7d, + 0x7f, 0x56, 0x31, 0xcc, 0x34, 0xd0, 0xb8, 0x0c, 0x29, 0x3f, + 0x83, 0x54, 0x02, 0x75, 0xe3, 0x79, 0xb6, 0x64, 0xf9, 0x66, + 0x7f, 0x90, 0xe1, 0xa7, 0x7b, 0xfa, 0x17, 0x68, 0x07, 0xd1, + 0xef, 0x35, 0x52, 0xf3, 0x56, 0xde, 0x35, 0x35, 0x14, 0x1b, + 0xcd, 0x97, 0x1a, 0xe5, 0xf4, 0xb5, 0xae, 0xfc, 0x53, 0x0c, + 0xec, 0x36, 0xf0, 0xb4, 0x30, 0xea, 0x9b, 0xde, 0x60, 0x31, + 0x72, 0x7e, 0x99, 0x41, 0x78, 0x90, 0x81, 0x73, 0x0a, 0x81, + 0xe2, 0xaa, 0x70, 0x68, 0x67, 0x7a, 0x52, 0x4a, 0x4d, 0xd4, + 0x9f, 0x90, 0x0d, 0xd9, 0x3a, 0x1f, 0x8d, 0x80, 0xfa, 0x68, + 0x62, 0x54, 0xae, 0xb7, 0x0f, 0x3a, 0xfa, 0xed, 0xc9, 0xce, + 0x20, 0x8d, 0x88, 0x48, 0x79, 0x82, 0x8a, 0xf6, 0xc5, 0x4d, + 0x6e, 0xed, 0x38, 0x81, 0x75, 0x65, 0xac, 0xf6, 0x72, 0xee, + 0xf0, 0x2e, 0x6d, 0x3f, 0x2e, 0x2a, 0x54, 0xf2, 0xf1, 0x5a, + 0xb9, 0x81, 0xce, 0x56, 0xbf, 0xed, 0xab, 0xd5, 0x32, 0xf1, + 0x95, 0x86, 0x81, 0xf9, 0xfd, 0x38, 0x1d, 0xc7, 0x9f, 0x55, + 0xe1, 0x2c, 0xd1, 0x28, 0x66, 0x26, 0x81, 0xf5, 0x12, 0x72, + 0x02, 0x35, 0x87, 0x65, 0x01, 0xea, 0x4f, 0xf7, 0x95, 0xba, + 0xb1, 0x09, 0x91, 0x9c, 0x89, 0xb4, 0x48, 0x1d, 0x7b, 0xb4, + 0x37, 0x76, 0x6c, 0xce, 0x83, 0x0e, 0x2c, 0xb8, 0xd5, 0xe0, + 0x43, 0x36, 0x3c, 0x2d, 0xb1, 0x0f, 0x29, 0x4e, 0x1d, 0x36, + 0x58, 0x9a, 0xcd, 0xf4, 0xa7, 0xad, 0xae, 0xdb, 0x8f, 0x59, + 0xd0, 0x78, 0xb4, 0xcb, 0xfd, 0xe6, 0x1b, 0xa9, 0x24, 0x26, + 0xb7, 0xa2, 0xc0, 0xbd, 0x08, 0x53, 0xd2, 0x4e, 0xb2, 0x19, + 0x90, 0xb1, 0xb0, 0xa7, 0x57, 0xaa, 0xeb, 0x11, 0x71, 0xdd, + 0x3b, 0xae, 0x04, 0xf4, 0x44, 0xa5, 0x27, 0xeb, 0xb3, 0x2e, + 0xdd, 0x70, 0x7e, 0x0d, 0x2b, 0x2f, 0xc4, 0x4c, 0xee, 0xff, + 0x4d, 0x94, 0xde, 0x6f, 0x48, 0xa1, 0x7b, 0xa2, 0xa2, 0xef, + 0x3a, 0xa3, 0x0c, 0x99, 0x4e, 0x6e, 0xd4, 0x64, 0xbb, 0xd2, + 0xa0, 0x38, 0x39, 0xe8, 0x16, 0xa3, 0x4b, 0x77, 0xb7, 0xa4, + 0xf6, 0xc0, 0x12, 0xec, 0xfe, 0x97, 0xc0, 0xa4, 0xfc, 0x90, + 0x8c, 0x07, 0xfb, 0x0a, 0xc9, 0x4f, 0xe8, 0xf7, 0x6e, 0x45, + 0xf6, 0xab, 0x8b, 0xb1, 0x58, 0x56, 0x5b, 0xda, 0x12, 0xc0, + 0x4c, 0xe1, 0x74, 0x1c, 0xb9, 0x14, 0xdf, 0xe4, 0x17, 0x53, + 0xb3, 0x0c, 0xe2, 0xe1, 0xbc, 0xfc, 0xe6, 0x9c, 0xed, 0xed, + 0x82, 0x20, 0x2a, 0x4d, 0xdf, 0xd8, 0x78, 0x6e, 0xf5, 0x16, + 0x57, 0x48, 0x58, 0x9a, 0xb6, 0x57, 0x20, 0xf0, 0x74, 0xcc, + 0xcf, 0xdd, 0x5c, 0xf4, 0xd6, 0x7e, 0xc4, 0x53, 0x13, 0xa4, + 0xf1, 0x68, 0xc5, 0xa4, 0xe1, 0x91, 0x58, 0xcf, 0x5c, 0x9f, + 0x36, 0xbf, 0x20, 0x21, 0x1b, 0x34, 0x5c, 0xbd, 0x2c, 0x64, + 0xc5, 0x6f, 0x7f, 0xb9, 0x91, 0x65, 0xcf, 0x25, 0x44, 0x93, + 0x99, 0x1f, 0xb9, 0x82, 0xeb, 0xc5, 0x3b, 0x4f, 0xd2, 0xad, + 0x5e, 0xaf, 0x69, 0xfc, 0x2f, 0x54, 0xe4, 0xd0, 0x34, 0xdb, + 0x47, 0xef, 0x8b, 0x16, 0x54, 0x2b, 0x67, 0x27, 0x3c, 0x50, + 0x9f, 0xf0, 0x6c, 0xe6, 0xe2, 0x08, 0xd0, 0x0a, 0x36, 0x76, + 0xc1, 0x65, 0x62, 0x01, 0x4d, 0x70, 0xb8, 0x42, 0xe4, 0x88, + 0xee, 0x63, 0x0b, 0x74, 0x64, 0x2b, 0x15, 0xf1, 0x90, 0x6c, + 0xa3, 0x3f, 0xf4, 0x94, 0xca, 0x3f, 0x48, 0x64, 0x5a, 0x09, + 0x36, 0x5c, 0x7c, 0x4f, 0x75, 0xf0, 0x4e, 0xe3, 0x3d, 0xf3, + 0x6d, 0x9b, 0x4f, 0x99, 0x29, 0xf1, 0xe2, 0xff, 0x92, 0x38, + 0x5d, 0xf6, 0xf3, 0xd2, 0xc5, 0x5d, 0x1d, 0x89, 0x78, 0x09, + 0xbb, 0x6a, 0x3a, 0x0a, 0x9c, 0x01, 0x4c, 0xd9, 0xfd, 0x55, + 0x48, 0xa1, 0xf7, 0xa1, 0x9c, 0x55, 0xc1, 0x31, 0xaf, 0xa5, + 0x03, 0xbb, 0xba, 0x8c, 0x76, 0x0a, 0xf0, 0xd6, 0x03, 0x57, + 0x71, 0xf5, 0xfe, 0xad, 0x7f, 0x36, 0x23, 0x57, 0xba, 0x9b, + 0x20, 0x92, 0x14, 0x9e, 0xcf, 0x76, 0x59, 0xcd, 0xe2, 0x47, + 0x12, 0xb4, 0x1e, 0xb5, 0x52, 0xc3, 0x7f, 0xda, 0xa0, 0x35, + 0xc2, 0x55, 0x56, 0x97, 0xd0, 0x57, 0xa0, 0xcc, 0xcd, 0x27, + 0xcc, 0x08, 0x1c, 0x9c, 0x06, 0xfd, 0xbe, 0x47, 0xdf, 0x78, + 0x08, 0x17, 0xb6, 0x7f, 0x31, 0x1e, 0x0a, 0xdc, 0x54, 0xcc, + 0x06, 0xf7, 0xbf, 0xee, 0x74, 0x48, 0x21, 0x39, 0x6c, 0xf0, + 0x1f, 0x29, 0x19, 0x76, 0x19, 0x7f, 0xc2, 0x38, 0x94, 0x8a, + 0xe4, 0x2d, 0x7f, 0xff, 0xf7, 0x45, 0x57, 0x21, 0x98, 0xaa, + 0x65, 0x35, 0xba, 0x3c, 0x56, 0xff, 0x62, 0x88, 0xb9, 0xe8, + 0xe9, 0xdb, 0xd1, 0xf8, 0x59, 0x0f, 0x50, 0x13, 0x8b, 0xc2, + 0xc2, 0xbd, 0xba, 0x44, 0x44, 0x36, 0xe1, 0xb0, 0x13, 0x32, + 0x3a, 0x68, 0x4d, 0x62, 0xa8, 0x50, 0x87, 0x44, 0xbb, 0x31, + 0xfb, 0xe3, 0x25, 0x73, 0xa0, 0xf1, 0x2c, 0xf5, 0x34, 0x19, + 0x09, 0x66, 0xa9, 0x46, 0x21, 0xc7, 0xa2, 0x91, 0x7d, 0xe9, + 0xf4, 0x9d, 0xa9, 0x82, 0x28, 0xf4, 0x1f, 0x28, 0xa8, 0xa9, + 0x01, 0xf1, 0x97, 0x70, 0x9d, 0x61, 0x72, 0xd5, 0x8b, 0x84, + 0xcf, 0xf1, 0x7c, 0xc2, 0x7d, 0xa7, 0xa3, 0x25, 0x4f, 0x68, + 0x3f, 0x5e, 0x8e, 0xb6, 0x16, 0x18, 0x78, 0xcf, 0x63, 0x53, + 0x57, 0x08, 0x11, 0x55, 0x65, 0x1b, 0x55, 0x5e, 0x8d, 0xce, + 0x68, 0x2d, 0x77, 0x16, 0x1e, 0x87, 0xa6, 0xd0, 0x11, 0x44, + 0x37, 0x8f, 0xa2, 0x8b, 0xb0, 0xdb, 0x4b, 0x01, 0x2f, 0xd7, + 0x1e, 0x16, 0x93, 0xb5, 0x19, 0xad, 0xb7, 0xde, 0x13, 0x0d, + 0xac, 0xcd, 0x77, 0x6a, 0x15, 0x61, 0xeb, 0xe0, 0xd1, 0xbf, + 0xcf, 0x69, 0x10, 0x28, 0x72, 0x58, 0xfe, 0xc7, 0xce, 0xbc, + 0x92, 0xfd, 0x5f, 0x9c, 0xd6, 0x6d, 0x56, 0xbd, 0x39, 0x35, + 0xe0, 0xa6, 0x73, 0x19, 0x45, 0x6a, 0xf2, 0x36, 0x8a, 0x0a, + 0x58, 0xe1, 0x23, 0x07, 0x28, 0xa4, 0x89, 0x7a, 0x51, 0xbb, + 0xba, 0xd6, 0x42, 0x78, 0xbb, 0x5f, 0x42, 0x0b, 0xc0, 0x8f, + 0xc9, 0x3c, 0xc3, 0x7e, 0x41, 0x22, 0x94, 0x3c, 0x02, 0xd3, + 0x2c, 0xec, 0xf8, 0x8f, 0x14, 0x62, 0x7c, 0x78, 0x47, 0x73, + 0xe8, 0xd5, 0x40, 0x2d, 0x26, 0x82, 0x72, 0x16, 0xd7, 0xdd, + 0x9e, 0x8d, 0x8e, 0xfb, 0xaf, 0x83, 0x73, 0xd8, 0x4d, 0x85, + 0x28, 0x53, 0x72, 0xc1, 0xb0, 0x08, 0x5a, 0xc6, 0x6e, 0xbe, + 0xf9, 0x21, 0xdd, 0x8c, 0x30, 0xf5, 0x5e, 0xbe, 0xec, 0xcc, + 0xd2, 0x8a, 0x1a, 0xc3, 0xf4, 0x83, 0x98, 0x86, 0x3b, 0x4e, + 0x6d, 0x3e, 0x9e, 0xbe, 0x84, 0x80, 0x2f, 0x27, 0xbb, 0x6a, + 0x3e, 0x51, 0x18, 0xb0, 0x7a, 0x82, 0x82, 0x02, 0x53, 0x6d, + 0x2d, 0x2a, 0xab, 0x4a, 0x2e, 0x3b, 0xa4, 0x88, 0x90, 0x99, + 0x74, 0x10, 0x27, 0xa7, 0xe9, 0x5c, 0x52, 0x5b, 0xf5, 0xb0, + 0x00, 0xc7, 0x52, 0x91, 0x04, 0xc7, 0xff, 0x01, 0xb7, 0x56, + 0x40, 0x9f, 0x56, 0x54, 0xde, 0x96, 0x65, 0x61, 0x76, 0x06, + 0xb6, 0xdb, 0x16, 0xc8, 0x1c, 0x0b, 0xff, 0xf0, 0x73, 0xd0, + 0x89, 0x22, 0x86, 0xc6, 0xac, 0xc5, 0xb1, 0x98, 0x21, 0xd0, + 0x6c, 0x30, 0x92, 0x43, 0xb4, 0xa9, 0x80, 0xc8, 0xdd, 0x6f, + 0xf0, 0x4b, 0x57, 0x80, 0x6d, 0x53, 0xfb, 0xd4, 0x95, 0x39, + 0xde, 0x6e, 0xf5, 0x28, 0x9e, 0x92, 0x94, 0x42, 0x99, 0x42, + 0x4d, 0x02, 0x0a, 0x7b, 0x9b, 0x8f, 0xc6, 0x32, 0xb0, 0xd9, + 0x5d, 0x81, 0x74, 0xd2, 0x76, 0xdb, 0x64, 0x21, 0xdf, 0xc2, + 0xee, 0x5f, 0x77, 0x88, 0x13, 0x60, 0x32, 0xd8, 0x97, 0xf4, + 0x1c, 0x77, 0xe0, 0x49, 0xce, 0x1d, 0xa2, 0xbb, 0x66, 0xca, + 0x27, 0xfd, 0xc1, 0x96, 0x3a, 0x50, 0x4f, 0x1f, 0xbb, 0x56, + 0x24, 0x85, 0x76, 0x77, 0xfd, 0x84, 0x7b, 0xb4, 0x3c, 0x87, + 0x01, 0x09, 0x89, 0x4c, 0x0f, 0x8e, 0x44, 0xc0, 0x49, 0x39, + 0x49, 0x8b, 0x93, 0x09, 0xa6, 0x8d, 0x93, 0xf1, 0x5a, 0x3f, + 0x1f, 0x64, 0x2f, 0xd2, 0xe2, 0xbe, 0x99, 0x38, 0x38, 0xf1, + 0xec, 0x15, 0x46, 0xe3, 0x8a, 0x95, 0xe6, 0x3c, 0xf8, 0xa3, + 0x38, 0xb8, 0xc0, 0x20, 0x5f, 0xcb, 0x36, 0xe4, 0x90, 0x79, + 0x0c, 0x8e, 0xb6, 0xe5, 0x48, 0x23, 0x2d, 0xcb, 0x3c, 0x88, + 0x31, 0x35, 0xe7, 0x8e, 0xfa, 0xf5, 0x81, 0x36, 0x35, 0x96, + 0x98, 0x39, 0xb0, 0x2d, 0xa3, 0xd7, 0x84, 0x3a, 0x92, 0x30, + 0x07, 0x9b, 0x70, 0x61, 0xda, 0xd6, 0x80, 0x94, 0x7b, 0x93, + 0x01, 0x10, 0xe2, 0x03, 0x23, 0x83, 0xc2, 0xef, 0x2f, 0x4e, + 0x31, 0x01, 0x87, 0x0c, 0x0b, 0x0c, 0x1f, 0x20, 0xaa, 0x75, + 0x52, 0xb5, 0xbc, 0xa2, 0x55, 0xd6, 0x06, 0x3f, 0xf1, 0x6a, + 0x1e, 0xa4, 0x00, 0x43, 0xdb, 0x0f, 0x9a, 0xd4, 0xc4, 0x45, + 0x2f, 0x20, 0x94, 0x57, 0xbc, 0x6e, 0xeb, 0x63, 0xda, 0x90, + 0xb2, 0x56, 0x9b, 0x6c, 0x81, 0xb3, 0x15, 0xd3, 0x1d, 0xe1, + 0xe4, 0x75, 0xaa, 0x3b, 0xbf, 0x45, 0x46, 0xd7, 0x0d, 0x31, + 0xb1, 0xf5, 0x8b, 0xfb, 0x5b, 0x3e, 0x48, 0x4b, 0xea, 0x09, + 0x0c, 0x82, 0xd3, 0x7a, 0x45, 0x1c, 0xc2, 0x4d, 0xda, 0x4d, + 0x08, 0xd1, 0x5d, 0x7c, 0xb5, 0x50, 0x24, 0xbf, 0x3b, 0x5a, + 0xb7, 0xca, 0x76, 0xc4, 0xee, 0x64, 0xf7, 0xc7, 0x77, 0x2d, + 0x06, 0x8d, 0x9b, 0xee, 0x7c, 0x6b, 0xf5, 0x6b, 0x8f, 0x62, + 0x4d, 0x37, 0x66, 0x31, 0x5f, 0xde, 0x2b, 0xb2, 0xe4, 0x70, + 0xda, 0xeb, 0x49, 0xe4, 0x1b, 0x1f, 0xdf, 0x45, 0xee, 0x5a, + 0x69, 0x68, 0x59, 0xe5, 0x71, 0x14, 0xcc, 0x4e, 0x73, 0xa4, + 0x20, 0x38, 0x5c, 0xc7, 0xa7, 0x28, 0xd5, 0xa0, 0xbf, 0xfd, + 0x6a, 0x0f, 0x92, 0x6f, 0x02, 0x64, 0x49, 0x19, 0xe4, 0x18, + 0x23, 0xa2, 0xda, 0x9b, 0x75, 0x7b, 0xc8, 0xdb, 0x8f, 0x78, + 0x68, 0x4c, 0x67, 0x47, 0x82, 0xf6, 0x10, 0x44, 0x23, 0x36, + 0x74, 0x02, 0x58, 0xad, 0x75, 0xd4, 0xad, 0x59, 0x9e, 0x2e, + 0x99, 0x67, 0xc2, 0x5b, 0x89, 0x91, 0x01, 0x37, 0x2e, 0xfe, + 0x50, 0xc4, 0x6d, 0xe7, 0x8b, 0x34, 0x53, 0x22, 0xd0, 0xc3, + 0x1f, 0x16, 0xc9, 0x14, 0x2f, 0x0b, 0x76, 0xdf, 0x29, 0xb9, + 0x32, 0xd4, 0xcd, 0x5e, 0x04, 0xdb, 0xf5, 0x47, 0x3f, 0x99, + 0x26, 0xcb, 0x6b, 0xac, 0xae, 0x2b, 0xbc, 0x75, 0xf2, 0x8d, + 0x64, 0xff, 0xd2, 0x2b, 0xfb, 0x8c, 0x81, 0x79, 0xed, 0x8d, + 0x86, 0x89, 0xc7, 0xfd, 0xe8, 0xde, 0x1a, 0xe1, 0x96, 0x30, + 0xba, 0xe3, 0x8d, 0xc8, 0x43, 0xd5, 0x97, 0x52, 0xe5, 0x19, + 0x0b, 0xd4, 0xba, 0x41, 0xea, 0x9a, 0xad, 0xcc, 0x94, 0x24, + 0x6e, 0x9c, 0x8f, 0xd1, 0x69, 0x6b, 0x4a, 0x50, 0x92, 0xba, + 0x73, 0x41, 0xb1, 0xd1, 0x82, 0x10, 0xbb, 0xcf, 0x3c, 0x4b, + 0xdf, 0x49, 0xdc, 0xdf, 0xd3, 0xbb, 0xa6, 0xac, 0x63, 0xbc, + 0x88, 0x6a, 0x4f, 0x3b, 0xfa, 0xd4, 0xdd, 0xd4, 0x3c, 0x3a, + 0xe5, 0x68, 0xd7, 0x86, 0xbd, 0xcd, 0xd3, 0x6d, 0xa8, 0x22, + 0xca, 0x71, 0xd6, 0xd0, 0xa7, 0xa7, 0x75, 0xd2, 0x70, 0xd4, + 0xb4, 0x27, 0x78, 0x2b, 0xcd, 0x16, 0x10, 0x81, 0xb1, 0x59, + 0xfe, 0xe4, 0x1b, 0x62, 0xd1, 0xcf, 0x63, 0x7c, 0x72, 0x78, + 0xc2, 0x96, 0xaf, 0x88, 0x76, 0x28, 0x0a, 0x47, 0xeb, 0xa8, + 0xd9, 0xd1, 0xef, 0xea, 0x01, 0x20, 0x15, 0x63, 0x89, 0xb4, + 0xe7, 0x99, 0xa0, 0x3b, 0x2f, 0xdf, 0x99, 0x0b, 0xc3, 0xef, + 0xa6, 0xf3, 0xe0, 0x90, 0xc9, 0x82, 0x4c, 0x3e, 0x51, 0xe9, + 0x2c, 0x86, 0x7d, 0x31, 0x7e, 0x8f, 0x5d, 0x7a, 0x24, 0xff, + 0x65, 0xa9, 0xb6, 0xbc, 0x18, 0xc8, 0x28, 0x4c, 0x8d, 0xda, + 0xc7, 0x98, 0x6a, 0x54, 0xad, 0x22, 0x19, 0xca, 0x3d, 0x4d, + 0x88, 0xcc, 0x4e, 0x5c, 0x2a, 0xef, 0xd0, 0x60, 0x3c, 0xe3, + 0x49, 0xd2, 0xf6, 0x59, 0x38, 0x25, 0xed, 0x31, 0x52, 0x18, + 0xb4, 0x31, 0x31, 0x43, 0x8f, 0x85, 0x7d, 0x59, 0xa8, 0xd2, + 0x26, 0x4c, 0xa0, 0xfb, 0xf8, 0xdb, 0x6a, 0x12, 0xad, 0x40, + 0x73, 0xb1, 0x5d, 0x61, 0xf5, 0x49, 0x65, 0x7b, 0x44, 0x30, + 0x49, 0x7a, 0xd7, 0xe9, 0xce, 0x34, 0x9c, 0x64, 0x6e, 0x31, + 0xae, 0x48, 0xad, 0x4c, 0x69, 0xc4, 0xd9, 0xf8, 0x2a, 0xb0, + 0x7e, 0xb3, 0xba, 0xec, 0xea, 0x2b, 0x49, 0x09, 0x21, 0x53, + 0x63, 0xb7, 0xc6, 0xd6, 0x80, 0x9c, 0x77, 0x27, 0xe9, 0xf6, + 0xc7, 0x06, 0x2c, 0x8e, 0xc1, 0xb8, 0x31, 0x15, 0xa9, 0x87, + 0x13, 0xfd, 0xfd, 0x32, 0x18, 0xdd, 0x5c, 0xba, 0xf3, 0x70, + 0x94, 0x09, 0x9d, 0xb6, 0xbc, 0x2c, 0x91, 0xef, 0x42, 0xb7, + 0x13, 0x25, 0x90, 0x77, 0x10, 0xdd, 0x80, 0xa9, 0xd9, 0xab, + 0xef, 0xcf, 0xbe, 0x75, 0x93, 0xe2, 0xbc, 0xe7, 0x26, 0xdf, + 0x77, 0x88, 0xe0, 0x90, 0xc0, 0x2d, 0x20, 0xc9, 0x8c, 0xfa, + 0x82, 0x91, 0x57, 0xfb, 0xb1, 0xec, 0xdf, 0x2b, 0x2f, 0x9b, + 0x6a, 0x23, 0xa0, 0x3e, 0x2a, 0xa3, 0x39, 0x8c, 0xea, 0x09, + 0x06, 0x03, 0x7b, 0xa8, 0xe7, 0x77, 0x32, 0x21, 0x1a, 0x29, + 0xcd, 0x22, 0x60, 0x9b, 0xc0, 0xfe, 0xc2, 0xf3, 0x9c, 0x83, + 0x56, 0x8f, 0xf0, 0x22, 0xad, 0xfd, 0xe3, 0xa9, 0xe3, 0xc5, + 0xbf, 0xd1, 0xf3, 0xbd, 0xc6, 0x43, 0xc3, 0x84, 0xf2, 0x46, + 0x91, 0x5b, 0x37, 0x43, 0xb9, 0xcf, 0xe4, 0x47, 0x7a, 0x5a, + 0xfd, 0xd2, 0xc0, 0xb6, 0x7e, 0x99, 0xea, 0x15, 0x1b, 0x31, + 0x3c, 0x14, 0x6b, 0xbf, 0x26, 0x4f, 0xfa, 0x38, 0x39, 0xef, + 0x75, 0x4c, 0xda, 0x4c, 0x4f, 0x00, 0xef, 0x0f, 0xc6, 0x3b, + 0x5b, 0x4c, 0xf1, 0x1b, 0x35, 0x92, 0xb9, 0x74, 0x9e, 0x0f, + 0x16, 0x2a, 0x2b, 0x3c, 0xa0, 0x80, 0xf0, 0xe5, 0x56, 0xaa, + 0xdb, 0x0e, 0x4f, 0xfc, 0x5f, 0xb2, 0xd1, 0x0b, 0xe2, 0x04, + 0x1e, 0xe8, 0x6f, 0x4f, 0x81, 0xdb, 0xc7, 0x1a, 0x0c, 0xb0, + 0x00, 0xed, 0x8a, 0x7b, 0x1d, 0x44, 0xde, 0x30, 0xfa, 0xb6, + 0x88, 0x3b, 0x73, 0x81, 0xb3, 0x57, 0x31, 0x38, 0x0e, 0x13, + 0x77, 0x9e, 0x5a, 0x4d, 0xa1, 0x93, 0x73, 0x30, 0x0d, 0x44, + 0x56, 0x97, 0x88, 0xbf, 0xc4, 0x36, 0xe9, 0xec, 0xe2, 0x58, + 0x11, 0x39, 0x33, 0x71, 0x6b, 0xd4, 0xb7, 0x4a, 0x9a, 0x2c, + 0xc0, 0xac, 0x88, 0xeb, 0x08, 0x8c, 0x43, 0x48, 0xa2, 0xba, + 0xfa, 0x98, 0x95, 0x92, 0x2d, 0xa2, 0xee, 0x72, 0xb3, 0x7e, + 0xd9, 0x11, 0x1a, 0xa3, 0x41, 0xc4, 0x65, 0x19, 0x1d, 0xdb, + 0x92, 0xf4, 0x3e, 0xf4, 0xaa, 0x49, 0xad, 0x77, 0xc6, 0x03, + 0xdd, 0x19, 0x7a, 0x8d, 0x62, 0x09, 0x19, 0xd7, 0xf7, 0xd3, + 0x84, 0xf1, 0xcb, 0x45, 0x54, 0xd1, 0x74, 0x1e, 0xf8, 0xe6, + 0xa4, 0xfd, 0xaa, 0x30, 0xf8, 0xf9, 0xe1, 0x2b, 0xcf, 0x65, + 0x22, 0xf9, 0x73, 0xcf, 0x5d, 0xe3, 0xde, 0x93, 0x94, 0x84, + 0x8f, 0xe1, 0x66, 0x99, 0x6a, 0x71, 0x76, 0xae, 0xdb, 0x16, + 0x2b, 0x73, 0xab, 0x4e, 0x61, 0x08, 0xa0, 0xdc, 0x3f, 0xd4, + 0xe3, 0xac, 0x89, 0x7a, 0x31, 0xc1, 0xb9, 0x20, 0x92, 0x10, + 0x58, 0x1b, 0x87, 0xfd, 0xe3, 0x7c, 0x42, 0xc8, 0x26, 0x06, + 0x50, 0xce, 0xbb, 0x00, 0x0b, 0x9f, 0x09, 0xdc, 0xac, 0x31, + 0xe0, 0xcd, 0xa6, 0xee, 0x23, 0xef, 0x64, 0x87, 0x65, 0x5f, + 0xfb, 0x5f, 0x6c, 0x29, 0x4a, 0xf9, 0xd3, 0xaf, 0x4e, 0xaa, + 0x39, 0x30, 0x82, 0x05, 0xc6, 0x1c, 0x1e, 0x83, 0x8f, 0xe4, + 0xf7, 0x1c, 0xca, 0x3b, 0x03, 0x01, 0x80, 0xe4, 0x6a, 0x84, + 0x71, 0xd4, 0xe9, 0xa6, 0xba, 0x72, 0x89, 0x69, 0xfa, 0x56, + 0x6d, 0x26, 0xe3, 0x1d, 0xfa, 0x91, 0x8f, 0x4c, 0xcc, 0x50, + 0x74, 0x75, 0x2f, 0xe3, 0x97, 0x16, 0x07, 0x78, 0x40, 0xde, + 0x58, 0x22, 0xca, 0x41, 0xde, 0xb9, 0x92, 0x56, 0x55, 0x89, + 0x85, 0x20, 0x44, 0x5a, 0xd0, 0xfc, 0x88, 0x4e, 0x49, 0x81, + 0x32, 0xcf, 0x44, 0xb6, 0xee, 0x58, 0x0e, 0x22, 0xf8, 0x8f, + 0x05, 0x2a, 0x78, 0x30, 0x34, 0x11, 0xb0, 0x8f, 0x21, 0x10, + 0xb7, 0xe2, 0x80, 0xa2, 0xe0, 0x69, 0xbc, 0x29, 0xe3, 0x48, + 0x08, 0xa6, 0xdc, 0xbc, 0x68, 0xd7, 0xd7, 0x84, 0x23, 0x3f, + 0xb0, 0x0a, 0x69, 0xa1, 0x87, 0x13, 0x29, 0x0d, 0x71, 0x3d, + 0xad, 0xad, 0xf0, 0x5e, 0x53, 0x04, 0x03, 0x9e, 0x4b, 0xb2, + 0x24, 0xc2, 0x82, 0x8c, 0xef, 0x4c, 0x70, 0xcd, 0x58, 0xb3, + 0x19, 0xa4, 0xe8, 0x0e, 0xc0, 0xfd, 0x57, 0x2b, 0x65, 0xee, + 0xa5, 0x43, 0x02, 0xf2, 0xbe, 0xe7, 0x66, 0xc9, 0xb5, 0xfd, + 0x70, 0xc0, 0x93, 0x9b, 0xe8, 0x60, 0x74, 0xd4, 0xcc, 0xa5, + 0xa6, 0xa2, 0x66, 0x35, 0xdc, 0x74, 0xf3, 0x02, 0x16, 0x74, + 0x9a, 0x91, 0x0a, 0x3d, 0xa4, 0x8b, 0x98, 0xd1, 0xfd, 0x29, + 0x34, 0xea, 0x52, 0x89, 0x16, 0x62, 0xdd, 0xed, 0xb5, 0x8b, + 0xed, 0xe4, 0x03, 0xd9 + }; + + static const uint8_t static_keys_b[0x70 * 0x20] = { + 0x76, 0x65, 0x9d, 0xe4, 0xf7, 0x6c, 0x45, 0x1f, 0x73, 0x42, + 0x47, 0xc3, 0x92, 0x32, 0xf4, 0x38, 0x6f, 0x28, 0xcf, 0xfa, + 0x41, 0x84, 0xbe, 0x1b, 0x5a, 0x23, 0x99, 0xfa, 0xcd, 0x74, + 0xf0, 0x3b, 0x43, 0xc9, 0xd1, 0x30, 0x66, 0x77, 0x5e, 0xac, + 0x39, 0x8e, 0x8a, 0x6c, 0x5c, 0xfe, 0xdd, 0x80, 0xf3, 0x03, + 0x04, 0x30, 0x84, 0x9d, 0x12, 0xee, 0x4a, 0xe3, 0x0f, 0x5e, + 0x5e, 0x97, 0xd0, 0xf9, 0x1b, 0x57, 0x05, 0x74, 0x03, 0x5a, + 0x5d, 0x41, 0xda, 0x6e, 0xfd, 0x4b, 0x1e, 0xc4, 0x4b, 0x76, + 0xe4, 0xd3, 0x39, 0xeb, 0x3c, 0x2c, 0xae, 0x27, 0xe2, 0x98, + 0xe4, 0x08, 0xa3, 0xbe, 0xb0, 0x31, 0x69, 0x14, 0xa3, 0x6c, + 0xdf, 0xfc, 0x1f, 0x56, 0x0d, 0xfe, 0x55, 0x12, 0x97, 0x5e, + 0xad, 0x3d, 0x9b, 0xee, 0xb5, 0xad, 0xa3, 0x56, 0x85, 0xc3, + 0xbd, 0x9b, 0xef, 0xac, 0x55, 0x98, 0x1f, 0xaf, 0xb8, 0x0b, + 0xae, 0xed, 0xf3, 0x87, 0x34, 0xc9, 0x5b, 0x69, 0xf0, 0xed, + 0x4f, 0x03, 0x40, 0xa5, 0x71, 0x24, 0xa2, 0x23, 0x93, 0x52, + 0xe9, 0x87, 0xc4, 0x79, 0x4f, 0xe9, 0x20, 0xbc, 0x04, 0x58, + 0x70, 0x96, 0xb1, 0xd7, 0x37, 0xc4, 0x29, 0x4d, 0x3d, 0x52, + 0xd3, 0xcf, 0x7c, 0x08, 0xe9, 0x82, 0xc6, 0x99, 0xf5, 0xdb, + 0x60, 0x11, 0x9b, 0x9d, 0x25, 0x86, 0x08, 0xb3, 0x77, 0x27, + 0xa8, 0x2c, 0xf9, 0x28, 0x2f, 0x2b, 0x70, 0x6c, 0x4b, 0xbb, + 0x1e, 0x49, 0x3e, 0xd6, 0x69, 0xd8, 0xe8, 0x09, 0xf4, 0x9c, + 0x09, 0x32, 0xdf, 0xdd, 0xe1, 0x0b, 0x41, 0x0e, 0x26, 0x3e, + 0x53, 0xcc, 0x7a, 0xd1, 0x15, 0x16, 0xfb, 0xc3, 0x09, 0xd3, + 0xad, 0xa4, 0xea, 0xa3, 0x4a, 0xbe, 0xf2, 0x88, 0x9a, 0x34, + 0x7b, 0x09, 0x41, 0x44, 0xf1, 0x5e, 0x3c, 0x4f, 0x0e, 0x9f, + 0x5a, 0x95, 0x57, 0x7a, 0xfe, 0xbe, 0xc8, 0x24, 0x1f, 0x98, + 0x85, 0x1e, 0x25, 0x74, 0x90, 0xdd, 0x5a, 0xd1, 0x7c, 0x02, + 0xbe, 0x63, 0xe5, 0xc7, 0xa2, 0x0b, 0x26, 0xdf, 0x20, 0x84, + 0x61, 0xe3, 0xff, 0x2c, 0x69, 0x32, 0x91, 0x62, 0xf8, 0x11, + 0x7c, 0xf2, 0x1e, 0xe5, 0x1e, 0xc1, 0xb2, 0x2a, 0x84, 0x8e, + 0x3a, 0x78, 0xf6, 0xf6, 0x76, 0x76, 0x4e, 0x34, 0x4f, 0x24, + 0xf0, 0x8c, 0x34, 0xaf, 0x95, 0x7a, 0x35, 0x26, 0x5e, 0x28, + 0xe3, 0x2c, 0x66, 0xf9, 0x7f, 0x35, 0xfc, 0xad, 0xfe, 0x6e, + 0x4b, 0xe1, 0x5c, 0x48, 0xec, 0x1a, 0xba, 0xbd, 0xac, 0x73, + 0x7a, 0xe8, 0xbc, 0xad, 0xba, 0x5e, 0xa6, 0xf0, 0xb8, 0x5a, + 0x51, 0x2a, 0x97, 0xe4, 0x34, 0x8a, 0x3b, 0x64, 0x61, 0x97, + 0x57, 0x59, 0x29, 0x87, 0x76, 0x80, 0x78, 0x05, 0x91, 0xc0, + 0xa5, 0x5e, 0x7b, 0x5a, 0xa5, 0x7b, 0x2f, 0x8d, 0x07, 0x7d, + 0xfa, 0x75, 0x32, 0xe8, 0x48, 0x0c, 0x68, 0xbf, 0x70, 0xb3, + 0x79, 0x88, 0xc0, 0xbd, 0xbc, 0x49, 0x9a, 0x9a, 0x07, 0xb8, + 0xa3, 0xc2, 0xb4, 0x1d, 0xca, 0x24, 0x90, 0xe8, 0x2f, 0xe8, + 0x9f, 0x28, 0xba, 0x40, 0x17, 0x50, 0x56, 0xcf, 0x4e, 0xc8, + 0x82, 0xcb, 0x77, 0xf1, 0x74, 0x8d, 0xef, 0x0b, 0x4c, 0x1f, + 0x3d, 0x4d, 0xd2, 0x4e, 0x66, 0x2f, 0x13, 0xe7, 0x8a, 0xb6, + 0xea, 0xd1, 0xd7, 0x36, 0xf3, 0x51, 0x65, 0x07, 0xc1, 0x88, + 0x7d, 0xae, 0xf4, 0xb8, 0x89, 0xb5, 0xce, 0xdd, 0x27, 0xbe, + 0x6f, 0x6c, 0x81, 0x8f, 0x56, 0x02, 0x0d, 0x49, 0xe4, 0x3f, + 0x60, 0x93, 0x55, 0xa3, 0x68, 0x75, 0x5d, 0xc5, 0x5a, 0x09, + 0x90, 0xdc, 0x8f, 0xaa, 0x9f, 0x47, 0xf6, 0xa9, 0xad, 0xdb, + 0x5b, 0xe2, 0x64, 0xce, 0xc1, 0x26, 0xeb, 0xeb, 0xa0, 0x03, + 0x23, 0x61, 0x14, 0x9e, 0x72, 0x48, 0xc2, 0x10, 0x3d, 0x73, + 0xc3, 0xc9, 0x22, 0x01, 0x68, 0x2c, 0x3c, 0x98, 0xa5, 0x85, + 0x0d, 0xa4, 0xec, 0xc6, 0xfc, 0x7d, 0xff, 0x2c, 0xae, 0x60, + 0xc4, 0xb0, 0x71, 0xb9, 0xbc, 0xdb, 0x29, 0xc6, 0x78, 0x0b, + 0xf6, 0x82, 0x38, 0xed, 0x35, 0xb7, 0x92, 0x2f, 0xeb, 0xa9, + 0xe7, 0xc3, 0x1a, 0xa2, 0xae, 0x44, 0x51, 0x71, 0x6e, 0x8c, + 0x21, 0x9f, 0x9c, 0x5d, 0x98, 0x72, 0xa3, 0x2e, 0x74, 0x49, + 0x70, 0xaf, 0x85, 0x7b, 0x14, 0x66, 0x56, 0x99, 0x89, 0x07, + 0xc4, 0xa9, 0xc2, 0xc2, 0xb3, 0x39, 0xa6, 0xf5, 0x5b, 0x3f, + 0x30, 0xcc, 0x2d, 0x03, 0x22, 0xb4, 0x2a, 0x27, 0x1a, 0x46, + 0xbc, 0xe5, 0xf6, 0x19, 0x34, 0xc1, 0x06, 0x6f, 0x28, 0x84, + 0xe1, 0xe3, 0xb0, 0x21, 0x21, 0x84, 0x34, 0x33, 0x8d, 0x01, + 0x26, 0x44, 0x5f, 0xa3, 0x56, 0x19, 0x46, 0xc1, 0x78, 0x29, + 0x38, 0x0b, 0x9f, 0xe1, 0xf1, 0x2b, 0xfc, 0x01, 0x91, 0x71, + 0x1c, 0xa5, 0x9c, 0xb6, 0xe1, 0x5e, 0xfa, 0x26, 0x56, 0x3f, + 0xa8, 0xb2, 0xd1, 0xe4, 0xf3, 0x69, 0x08, 0x36, 0xac, 0xd6, + 0x67, 0xb6, 0xe6, 0xb8, 0xc7, 0xa6, 0x7a, 0x10, 0x49, 0xe7, + 0xba, 0x6f, 0xbf, 0xe8, 0x25, 0xaf, 0xc2, 0xdf, 0xf3, 0xa1, + 0x76, 0xf4, 0x2b, 0x8c, 0x6f, 0x01, 0xce, 0x96, 0x30, 0xd2, + 0x2d, 0xf8, 0x6c, 0xad, 0xed, 0x1e, 0xe5, 0x7a, 0xdf, 0x37, + 0xe6, 0xff, 0xed, 0x9d, 0x5f, 0xa9, 0xfb, 0xf1, 0xbc, 0xc7, + 0x47, 0x5a, 0x6f, 0x77, 0x83, 0x18, 0x0e, 0xba, 0x18, 0x2b, + 0x38, 0xb4, 0x36, 0x32, 0x36, 0xdc, 0x98, 0xec, 0x16, 0xa1, + 0x68, 0x42, 0x95, 0xee, 0xad, 0x43, 0x4c, 0x3d, 0x31, 0xcf, + 0xdb, 0x57, 0xae, 0x0f, 0x78, 0x52, 0xd8, 0xa6, 0x47, 0x4b, + 0xba, 0x94, 0xc2, 0xd2, 0x97, 0x56, 0x2b, 0xb0, 0xbd, 0x43, + 0x30, 0x05, 0x4b, 0x30, 0x7b, 0x8b, 0xe4, 0xee, 0x61, 0xc4, + 0xcd, 0xcb, 0x08, 0xb1, 0x16, 0x2e, 0xa3, 0xe4, 0x20, 0x0e, + 0xd5, 0x02, 0x17, 0x81, 0xba, 0x43, 0xf7, 0x02, 0x7a, 0x0e, + 0x77, 0x15, 0x45, 0x03, 0xaa, 0x84, 0x88, 0x56, 0xb1, 0xee, + 0x3a, 0x1f, 0xe6, 0x28, 0xfd, 0x9f, 0x05, 0x81, 0x8d, 0xbf, + 0x53, 0xa1, 0x5b, 0xbd, 0x9c, 0x79, 0x53, 0x28, 0x73, 0xba, + 0x0d, 0x72, 0xe7, 0x14, 0xed, 0x5d, 0xb7, 0x95, 0x09, 0xa0, + 0xbf, 0xef, 0x75, 0xa1, 0x20, 0xa4, 0x2d, 0x16, 0xa6, 0x58, + 0x61, 0x93, 0x3a, 0x62, 0x40, 0xca, 0x4e, 0xef, 0xe4, 0x31, + 0x18, 0xd1, 0x39, 0x35, 0x7c, 0x78, 0xe3, 0x6c, 0x76, 0x8a, + 0x37, 0xdd, 0xc3, 0x8e, 0x60, 0xe2, 0x0d, 0x67, 0xfc, 0xe7, + 0x37, 0x88, 0x48, 0xb6, 0xe4, 0x4b, 0x87, 0xe6, 0x04, 0x8c, + 0x06, 0xd8, 0xc7, 0x4e, 0x9a, 0x97, 0x9d, 0x9d, 0xa1, 0xfa, + 0x11, 0x27, 0x8c, 0x34, 0xe7, 0x02, 0x55, 0x0f, 0x6e, 0xc6, + 0xcd, 0x6b, 0xc4, 0xf0, 0xa1, 0x05, 0x6b, 0x3f, 0xc4, 0xc9, + 0x47, 0x95, 0x85, 0x1f, 0x80, 0x6a, 0x23, 0xe7, 0x24, 0x9f, + 0xc3, 0xd0, 0x9d, 0x17, 0xea, 0x2f, 0xfa, 0xfa, 0xe4, 0xa6, + 0x88, 0x9a, 0x57, 0x0c, 0x5b, 0xfe, 0xb4, 0x67, 0x3e, 0xf0, + 0x0a, 0x58, 0xa0, 0xc8, 0xc7, 0x16, 0xd6, 0x0b, 0x41, 0x9e, + 0xa9, 0xde, 0xbf, 0x17, 0x49, 0x08, 0x82, 0xc4, 0x4e, 0x56, + 0x53, 0xb0, 0x4e, 0x23, 0x1d, 0xa8, 0x07, 0xde, 0x98, 0xf8, + 0xe9, 0xec, 0x45, 0x3b, 0x50, 0xf6, 0xb6, 0xa3, 0xdc, 0xfa, + 0xa1, 0x86, 0xd3, 0xfa, 0xda, 0xb6, 0x1d, 0x27, 0xed, 0x3d, + 0x17, 0xc9, 0xcc, 0xf6, 0x2f, 0x7d, 0xea, 0x3b, 0x3d, 0x58, + 0xb7, 0xf0, 0x4a, 0xae, 0xf9, 0x2f, 0xa0, 0x80, 0xe9, 0xde, + 0x2e, 0x30, 0x3c, 0xd6, 0x46, 0xca, 0x50, 0x25, 0xf7, 0xa2, + 0x66, 0xec, 0xa5, 0x6e, 0x06, 0x6e, 0x00, 0x3e, 0x08, 0x6c, + 0xe6, 0x3b, 0xfb, 0xc8, 0xa7, 0xd0, 0x1e, 0xc8, 0x25, 0x24, + 0x11, 0x0b, 0x39, 0x0f, 0x70, 0xd9, 0xf8, 0xff, 0x54, 0x54, + 0x9d, 0x02, 0xab, 0x4c, 0x8c, 0x90, 0x94, 0xe5, 0x0e, 0xd5, + 0x62, 0x78, 0xab, 0xa5, 0x9c, 0x11, 0x1c, 0x70, 0x07, 0x9b, + 0x2a, 0xe4, 0xfb, 0xed, 0xfb, 0xcd, 0x92, 0x0d, 0xbc, 0x21, + 0x25, 0x33, 0x0b, 0xbf, 0xf3, 0x18, 0x93, 0x73, 0xed, 0xf3, + 0x88, 0x3b, 0x43, 0x97, 0xce, 0x19, 0x25, 0xf5, 0xbb, 0x71, + 0x26, 0x98, 0x2c, 0xd6, 0xff, 0x32, 0x4f, 0x75, 0xcc, 0x92, + 0x4e, 0xe3, 0xbe, 0xb8, 0xbf, 0x33, 0x89, 0xe5, 0x09, 0x4c, + 0x41, 0x8c, 0xb0, 0x51, 0x74, 0x32, 0xea, 0x96, 0xce, 0x70, + 0x1c, 0x5c, 0x26, 0x34, 0x66, 0x3a, 0x3a, 0xe8, 0x92, 0x4e, + 0xd2, 0x61, 0x70, 0x12, 0x3c, 0x33, 0x9d, 0x09, 0x57, 0x23, + 0x48, 0x81, 0x29, 0x7f, 0xc9, 0x4d, 0x06, 0xe8, 0xc5, 0x6e, + 0x50, 0x7c, 0x8f, 0xfa, 0xf0, 0xb4, 0xe6, 0x9c, 0x7d, 0x5e, + 0x09, 0x36, 0xb3, 0x24, 0xc4, 0xf8, 0xe3, 0x19, 0xa8, 0x65, + 0x3f, 0x2a, 0x84, 0x98, 0x79, 0x18, 0x58, 0x26, 0x29, 0x5e, + 0xbc, 0x3c, 0x7e, 0x80, 0xaa, 0x04, 0x43, 0x1b, 0xbd, 0x36, + 0xef, 0x5c, 0xc6, 0x78, 0xfb, 0xa6, 0xfd, 0x26, 0x6a, 0xa4, + 0xb6, 0x2e, 0x92, 0xb7, 0x7a, 0x02, 0xa7, 0xc8, 0xb0, 0x47, + 0xa8, 0x6e, 0x6a, 0x53, 0x15, 0x81, 0x7d, 0x1c, 0xb0, 0x55, + 0xbe, 0xa9, 0x11, 0x1e, 0xd0, 0xd8, 0x99, 0x37, 0xae, 0x35, + 0xfa, 0xe4, 0x0b, 0x4c, 0x52, 0x0c, 0x12, 0x3a, 0x5f, 0x09, + 0x4e, 0x21, 0xc6, 0x0d, 0x56, 0xe4, 0x23, 0xd2, 0x5e, 0x86, + 0x7f, 0x04, 0xb0, 0x1c, 0xeb, 0x9f, 0x50, 0x17, 0xcc, 0x8e, + 0xf9, 0x8f, 0x00, 0x91, 0xee, 0xa7, 0xa4, 0x91, 0x59, 0xb7, + 0x11, 0x9e, 0x87, 0x40, 0xe2, 0x41, 0x08, 0x0f, 0xec, 0x40, + 0xbc, 0x8c, 0x30, 0x14, 0xcc, 0x97, 0xc9, 0xf0, 0x50, 0x7b, + 0x5c, 0x18, 0xe2, 0x1b, 0x02, 0x7f, 0xda, 0xc0, 0xe6, 0x23, + 0x4b, 0x48, 0xbb, 0xe8, 0x01, 0xe9, 0xd4, 0xb8, 0x52, 0xfa, + 0x36, 0x99, 0xdd, 0x03, 0x8d, 0x21, 0xd3, 0x16, 0x17, 0x2c, + 0x25, 0x20, 0x70, 0x92, 0xd5, 0x57, 0xcb, 0xff, 0x70, 0x16, + 0x33, 0x18, 0xf3, 0x31, 0x1a, 0xb0, 0x07, 0x27, 0x82, 0xc5, + 0x13, 0x86, 0x3d, 0xb5, 0x22, 0xfb, 0x7d, 0x2a, 0x4d, 0x45, + 0xaa, 0x2f, 0x46, 0x67, 0x1e, 0x9f, 0xc4, 0x8b, 0x3a, 0xb8, + 0x8c, 0x56, 0x3c, 0xa4, 0xfe, 0xe7, 0x93, 0xb2, 0xed, 0xc8, + 0x19, 0x3c, 0xc0, 0x5d, 0x38, 0xb0, 0xa2, 0x54, 0xe8, 0x71, + 0xd1, 0x69, 0xc7, 0xb8, 0xdc, 0x33, 0x0c, 0x3b, 0x8f, 0xf4, + 0x37, 0x23, 0x3d, 0x1e, 0x7b, 0x50, 0xc5, 0x4c, 0x3a, 0x6a, + 0x8d, 0xa8, 0x2b, 0x3d, 0x8c, 0x63, 0x4e, 0x52, 0x79, 0xea, + 0x7f, 0xa2, 0x0c, 0xc0, 0xdc, 0xba, 0x60, 0x04, 0x86, 0xa5, + 0xd6, 0x2b, 0xab, 0x2a, 0x17, 0xbe, 0x95, 0x11, 0x2d, 0x14, + 0x4e, 0xc1, 0x42, 0xa3, 0x8e, 0xaf, 0x57, 0x4d, 0x84, 0xd9, + 0x3c, 0x91, 0xb9, 0x45, 0x09, 0x6a, 0xf5, 0xb3, 0x24, 0x4a, + 0x2d, 0x69, 0x46, 0x51, 0x65, 0x9a, 0x37, 0x11, 0x7d, 0xde, + 0x9c, 0x7c, 0x11, 0xaf, 0x81, 0xfa, 0xdf, 0x1e, 0x5d, 0xff, + 0xb2, 0x64, 0x33, 0x75, 0x23, 0x7f, 0x99, 0xc2, 0xa6, 0xb8, + 0x30, 0xdf, 0xd2, 0xb6, 0x5a, 0x35, 0x6b, 0xc5, 0x28, 0xb0, + 0x60, 0x1a, 0x85, 0x59, 0x2e, 0x2b, 0x8b, 0x8f, 0x9a, 0x4c, + 0xd2, 0x9b, 0x90, 0x1e, 0x95, 0x5a, 0xf3, 0xac, 0x34, 0x78, + 0x8c, 0xeb, 0x67, 0x09, 0x6b, 0x95, 0x73, 0x5e, 0x54, 0x1f, + 0x05, 0xd6, 0xe4, 0xf4, 0x3b, 0x4d, 0x93, 0x13, 0x8a, 0xa2, + 0x36, 0x62, 0xb9, 0x76, 0xdc, 0x77, 0xf7, 0x10, 0xf6, 0x73, + 0xb7, 0x49, 0x90, 0x53, 0x9a, 0xbe, 0x96, 0x9d, 0x4c, 0xd4, + 0xf5, 0x09, 0x15, 0x88, 0xe8, 0xc7, 0xbd, 0x04, 0x1e, 0xf8, + 0x70, 0x88, 0x7d, 0xf9, 0x2c, 0xf4, 0x80, 0x5c, 0x6f, 0x55, + 0x73, 0xf2, 0xa0, 0x90, 0xb4, 0xb4, 0xb9, 0x80, 0x49, 0x3a, + 0xc5, 0xda, 0x84, 0x8c, 0x26, 0x84, 0x90, 0xc1, 0x2b, 0xa6, + 0xac, 0x48, 0x2d, 0x37, 0x61, 0x27, 0xd2, 0x18, 0xb8, 0x51, + 0x28, 0x0b, 0x13, 0xbb, 0x35, 0x58, 0xe6, 0xf0, 0x24, 0x18, + 0x28, 0x42, 0x6d, 0xdd, 0xba, 0xe8, 0x64, 0xf2, 0xff, 0x47, + 0x27, 0x59, 0x16, 0x55, 0xcc, 0x5b, 0x54, 0xa3, 0xe2, 0xfb, + 0xef, 0x14, 0x68, 0x8d, 0xbf, 0x2e, 0x9a, 0xbc, 0xfa, 0xb8, + 0x8e, 0x5e, 0x37, 0x8a, 0x1b, 0x67, 0x09, 0xe8, 0x0b, 0xed, + 0x44, 0x71, 0x16, 0x05, 0xbc, 0x5b, 0x2a, 0xf8, 0x8a, 0xdd, + 0xd9, 0x74, 0x53, 0x73, 0xb0, 0xd9, 0xb9, 0xfd, 0xd6, 0x67, + 0x52, 0x7e, 0x2c, 0x50, 0x36, 0xd0, 0x3b, 0xf7, 0x1f, 0xd8, + 0x4a, 0x06, 0x11, 0xf3, 0x21, 0x1f, 0x6e, 0x8f, 0x24, 0x44, + 0xb8, 0xa1, 0xba, 0x0a, 0xbb, 0x6c, 0x18, 0x02, 0x8e, 0xe7, + 0x5b, 0xed, 0xaa, 0x39, 0xbb, 0xb5, 0x99, 0x00, 0xd9, 0x62, + 0x26, 0x5f, 0x26, 0x3e, 0x8e, 0x49, 0x11, 0xd4, 0x2e, 0x70, + 0x5e, 0x79, 0x08, 0x63, 0xfb, 0x8d, 0x70, 0x8f, 0x4f, 0x1d, + 0x4f, 0x33, 0x72, 0xcd, 0xb1, 0xbd, 0x20, 0x4c, 0x74, 0x7f, + 0xde, 0x54, 0xe3, 0x1e, 0xb1, 0x0d, 0xb1, 0xeb, 0x5c, 0x63, + 0xd6, 0xc9, 0xaf, 0x82, 0xa6, 0xc5, 0x19, 0xe7, 0x96, 0x2f, + 0xaf, 0x19, 0x79, 0x4a, 0xe4, 0x78, 0xd0, 0xb3, 0xfc, 0xff, + 0xd7, 0x8f, 0x35, 0x93, 0x41, 0x2d, 0x59, 0xf2, 0xd8, 0xcc, + 0x76, 0xb8, 0x3a, 0xf7, 0xdf, 0x1c, 0xa8, 0x85, 0xd5, 0xf8, + 0x54, 0x63, 0x75, 0x36, 0xea, 0x30, 0x5a, 0x13, 0x74, 0xf9, + 0x6d, 0x55, 0x27, 0x04, 0x0b, 0xa0, 0xa0, 0x6d, 0x9e, 0x3a, + 0x93, 0xbf, 0x4e, 0x92, 0x28, 0x85, 0xe4, 0xcf, 0xc5, 0x06, + 0xa7, 0x17, 0x5a, 0xec, 0xc2, 0x60, 0x6a, 0xab, 0x1b, 0x50, + 0xc9, 0x0b, 0x84, 0x9a, 0x82, 0x45, 0x1e, 0x29, 0xcc, 0x85, + 0x5a, 0xa8, 0xd4, 0x90, 0xfb, 0xa7, 0x2f, 0x89, 0xa7, 0x80, + 0xd0, 0x69, 0xb6, 0xca, 0xe0, 0x00, 0x4e, 0xe8, 0xea, 0x12, + 0x74, 0x85, 0xc8, 0x35, 0xea, 0x1d, 0x23, 0x75, 0x97, 0xcb, + 0x1d, 0x94, 0x6f, 0x2a, 0xa9, 0x7a, 0x32, 0xc5, 0x92, 0xf0, + 0x0a, 0x28, 0xbd, 0x77, 0xbc, 0xbe, 0xae, 0xa7, 0xa8, 0x34, + 0x96, 0x26, 0xaf, 0x83, 0x2a, 0x8e, 0xe9, 0xda, 0xad, 0xb2, + 0x52, 0x94, 0x74, 0x69, 0x70, 0x19, 0xc4, 0xc8, 0x36, 0xf3, + 0x0f, 0x32, 0x0f, 0x6f, 0x2b, 0x75, 0xac, 0x0e, 0x9d, 0x91, + 0x1f, 0x06, 0xba, 0xe3, 0x9c, 0x7f, 0xde, 0x16, 0x3a, 0x3f, + 0xfd, 0x7d, 0x88, 0x61, 0x90, 0xca, 0x98, 0xc1, 0xcf, 0xa5, + 0x50, 0x1f, 0xb3, 0x8c, 0xf9, 0x8c, 0xe9, 0x9f, 0xa3, 0xd3, + 0xd7, 0xce, 0x7f, 0xdf, 0xd2, 0x8e, 0x84, 0x9c, 0x24, 0xc3, + 0x85, 0xb1, 0xc6, 0x94, 0x8f, 0xbb, 0x55, 0x83, 0x8e, 0xa3, + 0xc7, 0xf5, 0x8e, 0x7e, 0xe6, 0x30, 0x13, 0x06, 0x61, 0x9e, + 0x63, 0x24, 0x27, 0x03, 0x01, 0xef, 0xec, 0x6d, 0xb1, 0x56, + 0x86, 0xd9, 0xf3, 0x2e, 0xfc, 0xe2, 0xbb, 0xa5, 0xa7, 0xaa, + 0x9c, 0x06, 0xf3, 0x36, 0x04, 0xae, 0xbd, 0x0f, 0x6f, 0xb9, + 0xc7, 0x42, 0x79, 0xfd, 0xac, 0x8f, 0x1c, 0xcc, 0x01, 0x9f, + 0x45, 0x3a, 0x52, 0xa2, 0x20, 0x31, 0xf6, 0x5d, 0xea, 0x4e, + 0xe7, 0x13, 0x9d, 0x79, 0xe8, 0x81, 0x62, 0xcd, 0xaf, 0x2e, + 0x6b, 0xb5, 0x1b, 0x75, 0xfe, 0x61, 0xf7, 0xcf, 0x4d, 0x90, + 0xdf, 0xe8, 0x56, 0x23, 0x96, 0xe4, 0xab, 0x5d, 0xa5, 0xd8, + 0x2e, 0x9e, 0x0c, 0x30, 0x27, 0xa3, 0x2c, 0x10, 0xda, 0x79, + 0xaf, 0xb9, 0x16, 0x6a, 0x89, 0x33, 0xe7, 0x46, 0xef, 0x43, + 0xe5, 0x5e, 0xb6, 0x38, 0xaa, 0x07, 0xb6, 0x5d, 0xd8, 0x57, + 0x4b, 0x94, 0xbd, 0xd0, 0x73, 0x9c, 0xf9, 0xa9, 0x31, 0x8a, + 0x71, 0xe5, 0xf0, 0xe6, 0xfb, 0x4e, 0xc4, 0xc7, 0x4b, 0xfe, + 0xe2, 0x1e, 0xbc, 0x77, 0xba, 0x3b, 0x2c, 0xa2, 0x71, 0x2e, + 0xf1, 0xd7, 0x33, 0xa2, 0xae, 0x2d, 0x89, 0x6a, 0xcb, 0x63, + 0xed, 0x8d, 0x83, 0x10, 0x96, 0xb4, 0x43, 0xef, 0xc2, 0xe6, + 0x49, 0x13, 0x2f, 0x0a, 0x09, 0xae, 0xca, 0xa8, 0x01, 0xff, + 0xc9, 0x65, 0x87, 0x8d, 0x20, 0x98, 0x5f, 0xfb, 0x25, 0xe0, + 0x7a, 0x3a, 0xcb, 0xf8, 0x48, 0x7e, 0x18, 0xcc, 0x4c, 0x66, + 0xd3, 0x76, 0x10, 0x93, 0xa2, 0x7d, 0x3b, 0x0d, 0x49, 0xca, + 0xa9, 0x18, 0x2a, 0x50, 0xe2, 0xb3, 0xdc, 0x77, 0x3c, 0x23, + 0x84, 0xfe, 0xe7, 0x86, 0x39, 0x7f, 0xc7, 0x18, 0xca, 0xae, + 0x79, 0xfc, 0x44, 0x27, 0xf9, 0xbc, 0xce, 0xdb, 0xda, 0xe9, + 0xd5, 0x83, 0x90, 0xfb, 0x8c, 0x79, 0x4b, 0xc9, 0x1b, 0x6b, + 0x57, 0x0a, 0x88, 0xfc, 0x80, 0x7d, 0x17, 0x10, 0x32, 0x7d, + 0xf5, 0x87, 0xf7, 0x18, 0xd6, 0x7a, 0x60, 0x5c, 0x2b, 0x4a, + 0x19, 0x0a, 0xea, 0x3f, 0xd1, 0x5b, 0x5e, 0x93, 0x38, 0x9f, + 0xc9, 0xfc, 0x6c, 0x67, 0x91, 0xd6, 0x0c, 0x3b, 0x1e, 0xa6, + 0x2d, 0x1a, 0x71, 0x74, 0x57, 0x67, 0x46, 0xa2, 0x93, 0xaf, + 0x9d, 0x85, 0x0e, 0x0b, 0x1d, 0x39, 0x84, 0xe1, 0x7c, 0x8a, + 0x11, 0xed, 0x7b, 0xfe, 0x98, 0xaf, 0x2b, 0x7a, 0xe6, 0xc6, + 0x70, 0xf7, 0xce, 0x13, 0xe6, 0x5a, 0x96, 0x7e, 0x5d, 0x76, + 0x30, 0xfc, 0x22, 0xdc, 0x4e, 0xb2, 0xd5, 0x59, 0x66, 0x54, + 0x70, 0x25, 0x83, 0x1f, 0x16, 0xf0, 0x0a, 0x72, 0xd5, 0xb1, + 0x69, 0x86, 0x13, 0xb8, 0x6c, 0x0f, 0xe0, 0x5a, 0xdd, 0x5e, + 0x26, 0x9a, 0x9e, 0xbc, 0x94, 0xdf, 0xd0, 0x08, 0x4f, 0xc9, + 0x87, 0xf3, 0xd5, 0x6d, 0xad, 0x65, 0xbd, 0x52, 0xd2, 0x10, + 0x71, 0xca, 0x4b, 0xe5, 0x72, 0xcd, 0xae, 0xa7, 0x9c, 0x51, + 0x52, 0x04, 0xe5, 0xcb, 0x80, 0xfa, 0x23, 0x1b, 0x4a, 0x0d, + 0x83, 0x8a, 0xa6, 0x91, 0xef, 0x10, 0x08, 0x05, 0x2a, 0x39, + 0x14, 0x2f, 0xdb, 0x1f, 0x96, 0x8d, 0xe1, 0x58, 0x08, 0xd0, + 0x65, 0x6d, 0x83, 0x8f, 0x71, 0xe0, 0x87, 0x90, 0x96, 0xce, + 0xa4, 0xa0, 0x30, 0xf7, 0x56, 0xd4, 0x88, 0x61, 0x08, 0x61, + 0xda, 0xd1, 0x36, 0x8a, 0x92, 0x3f, 0xb1, 0xd4, 0xd0, 0x6f, + 0x57, 0x9b, 0xb4, 0xe2, 0x96, 0x12, 0xad, 0x93, 0x4b, 0xd6, + 0x59, 0x37, 0x08, 0x4f, 0x7a, 0xc8, 0x7f, 0x37, 0x8e, 0x11, + 0x5f, 0x07, 0xd2, 0x5e, 0xbe, 0x54, 0xcb, 0xa8, 0xc6, 0x2e, + 0xfc, 0x23, 0x7e, 0xab, 0x40, 0x24, 0x19, 0x4c, 0x51, 0x19, + 0x52, 0xcc, 0xc2, 0x70, 0x63, 0xc6, 0x34, 0x1c, 0x97, 0x14, + 0xb1, 0xfe, 0xd3, 0xe6, 0x98, 0xf2, 0xd7, 0x77, 0x5a, 0x69, + 0xc6, 0xf6, 0x0f, 0xb9, 0x62, 0xae, 0x4a, 0xb0, 0x33, 0x3a, + 0x43, 0x16, 0x2b, 0x66, 0xbf, 0xc6, 0x8c, 0x41, 0x8d, 0x0f, + 0x94, 0x79, 0x77, 0x81, 0x01, 0x23, 0xd9, 0x5e, 0x81, 0x74, + 0x1d, 0x87, 0x11, 0x76, 0xc6, 0x0f, 0x40, 0x91, 0x72, 0xe9, + 0x7d, 0x51, 0xb8, 0x72, 0xa9, 0x6f, 0x23, 0x1d, 0x32, 0x33, + 0xde, 0xd8, 0xf7, 0x9c, 0x52, 0xa4, 0x6d, 0x4f, 0x3f, 0x9e, + 0xe4, 0xfa, 0x25, 0x8f, 0x27, 0x78, 0x8d, 0xde, 0x39, 0xa2, + 0xfd, 0xd1, 0x39, 0x01, 0xeb, 0xce, 0xac, 0x60, 0xe4, 0x9f, + 0x69, 0x8b, 0xf8, 0xd1, 0x85, 0x70, 0x74, 0xad, 0x1e, 0x30, + 0x89, 0x9f, 0x84, 0x58, 0xff, 0xd5, 0x84, 0x41, 0xa6, 0x68, + 0x16, 0xc8, 0x07, 0x9b, 0x7a, 0x33, 0x29, 0x05, 0x5c, 0x0e, + 0x08, 0x94, 0x4d, 0x18, 0x80, 0xba, 0x10, 0x82, 0x4e, 0x92, + 0x60, 0xa8, 0xff, 0x1a, 0x0e, 0x46, 0x57, 0x1e, 0x65, 0x67, + 0x25, 0x8d, 0x28, 0xb1, 0xdb, 0x66, 0xca, 0xc0, 0xbc, 0xa9, + 0x90, 0x8a, 0x74, 0x52, 0x97, 0x68, 0x32, 0x34, 0x60, 0xb0, + 0x30, 0x60, 0xd5, 0x89, 0x15, 0x09, 0x1f, 0x95, 0xc5, 0x41, + 0xe2, 0x02, 0x7a, 0x81, 0x7d, 0x49, 0xdd, 0x8c, 0x26, 0x05, + 0xfe, 0xed, 0xa8, 0x12, 0xa4, 0x3d, 0x82, 0x65, 0x3d, 0x87, + 0xed, 0x95, 0x86, 0x80, 0x3f, 0xc6, 0xcd, 0x27, 0x58, 0x56, + 0xca, 0x23, 0xca, 0xa4, 0xd8, 0x06, 0x11, 0xb4, 0x24, 0xc8, + 0x8a, 0xc5, 0x36, 0x42, 0xe3, 0xd4, 0xf7, 0xaf, 0x74, 0xac, + 0x89, 0xee, 0x47, 0x2e, 0x8b, 0xde, 0xd0, 0x85, 0xd4, 0x2e, + 0xb4, 0xae, 0x99, 0x96, 0x88, 0xe2, 0x60, 0x99, 0xfb, 0x62, + 0x5f, 0x8b, 0xa7, 0x24, 0xe2, 0xc3, 0x72, 0x4c, 0x6f, 0x1d, + 0xcd, 0xc1, 0x3a, 0x32, 0x06, 0x98, 0xb5, 0x92, 0x82, 0x98, + 0xb1, 0x7d, 0xca, 0xa1, 0xe1, 0x4d, 0xe1, 0xe5, 0xaf, 0x3a, + 0x08, 0x0d, 0x68, 0x33, 0xb8, 0xc0, 0xc3, 0xfd, 0xb9, 0x90, + 0x47, 0x58, 0xca, 0xb2, 0x8d, 0x2d, 0x75, 0x7d, 0xbb, 0xd3, + 0xc3, 0x0f, 0x7d, 0xa7, 0x5a, 0x5a, 0x25, 0x6f, 0x12, 0xd8, + 0x69, 0xd4, 0xc1, 0x20, 0x19, 0x65, 0x4d, 0x36, 0x7c, 0x7f, + 0xc4, 0x47, 0x00, 0xcb, 0x2c, 0x64, 0x13, 0x06, 0x0a, 0x37, + 0x1d, 0x1d, 0x7d, 0x89, 0x9c, 0x21, 0xd0, 0xaa, 0xa9, 0x01, + 0xee, 0xb2, 0x70, 0xc7, 0xc3, 0x11, 0x60, 0xd9, 0x73, 0xc9, + 0xea, 0x6d, 0x8a, 0x1b, 0x0f, 0x69, 0x1d, 0xb3, 0x95, 0x82, + 0x52, 0xf4, 0xa9, 0x4e, 0x25, 0x85, 0x75, 0xde, 0x12, 0x3c, + 0x11, 0x71, 0x06, 0x88, 0xf0, 0x39, 0x93, 0x29, 0x3a, 0x94, + 0x7c, 0x73, 0x42, 0xea, 0x59, 0x7c, 0x1d, 0x93, 0x1a, 0x14, + 0x10, 0x4c, 0x5e, 0x92, 0x1a, 0x9e, 0x71, 0xf1, 0x42, 0xb9, + 0x72, 0x13, 0x7d, 0x99, 0x4a, 0x28, 0x8b, 0x92, 0x48, 0x1d, + 0x78, 0x19, 0x04, 0xf8, 0xc4, 0xf3, 0xb1, 0x1c, 0x2f, 0x56, + 0xb6, 0xf6, 0x87, 0x00, 0xbd, 0x8c, 0x8c, 0x56, 0xe1, 0x38, + 0xf8, 0x6d, 0x19, 0xe7, 0x7e, 0x30, 0x52, 0xc6, 0x47, 0x72, + 0xc9, 0xa1, 0x30, 0xef, 0x8c, 0xf6, 0x23, 0xbf, 0xfb, 0xd0, + 0xbb, 0x51, 0xa9, 0xf6, 0x1d, 0xa7, 0xd2, 0xc7, 0xaa, 0xa6, + 0xc8, 0xa6, 0xa6, 0x61, 0x8f, 0x8c, 0x22, 0xee, 0x6e, 0xb5, + 0x21, 0x89, 0x72, 0x0e, 0xbe, 0x00, 0xcf, 0x08, 0x80, 0x09, + 0x06, 0x4d, 0xd4, 0xd4, 0xea, 0xb6, 0xa0, 0x8e, 0x71, 0xc8, + 0x73, 0x16, 0xdb, 0x55, 0x0f, 0xed, 0xbf, 0xb7, 0x40, 0x41, + 0x9a, 0x47, 0xfc, 0xad, 0x5b, 0xf4, 0x86, 0x3c, 0x98, 0xd1, + 0xaa, 0x51, 0x15, 0xc3, 0x10, 0x55, 0x8e, 0x5d, 0xf7, 0x9d, + 0xd4, 0x11, 0x35, 0xe7, 0x05, 0x11, 0x7d, 0x0f, 0x2b, 0x44, + 0xfe, 0x12, 0x7f, 0x39, 0xa6, 0x19, 0x91, 0x8b, 0xd0, 0xac, + 0x2c, 0x18, 0x63, 0x6a, 0x33, 0xc2, 0x75, 0xd2, 0xcd, 0xf7, + 0x22, 0x46, 0xca, 0xd1, 0xf9, 0xc0, 0x15, 0x08, 0xc7, 0x10, + 0x67, 0xd1, 0x42, 0x13, 0x27, 0x22, 0xc3, 0xe3, 0x94, 0x66, + 0x00, 0xbf, 0x2f, 0x7d, 0xfb, 0xc5, 0x6f, 0xb9, 0x03, 0x24, + 0xce, 0xfb, 0xf4, 0x6b, 0xad, 0xe0, 0x47, 0xda, 0x5f, 0xea, + 0x61, 0xfd, 0x6a, 0x76, 0x98, 0x15, 0x76, 0x72, 0xef, 0x95, + 0x77, 0x93, 0x3b, 0x23, 0xde, 0x09, 0x38, 0x1c, 0x7b, 0xd5, + 0xd1, 0x5c, 0xc5, 0xc4, 0x34, 0x0c, 0xd8, 0xc2, 0x05, 0xb7, + 0xe5, 0xb5, 0xc9, 0x7b, 0xc1, 0x28, 0xea, 0xc8, 0x07, 0x98, + 0xe2, 0xab, 0x73, 0x24, 0xfb, 0xef, 0xce, 0x0a, 0xa7, 0xb9, + 0x8d, 0xeb, 0x31, 0x09, 0x82, 0xf2, 0x9c, 0x4d, 0xf1, 0x1a, + 0x4a, 0x79, 0x35, 0xa7, 0x7b, 0x7b, 0xf0, 0xa9, 0xe7, 0xb9, + 0xf9, 0x52, 0x63, 0x73, 0x29, 0xe4, 0xd4, 0x13, 0x19, 0x2a, + 0xf2, 0x9d, 0x4e, 0xcc, 0x2a, 0xb3, 0xdf, 0x95, 0xc5, 0xb6, + 0x91, 0x34, 0xf4, 0x10, 0x2e, 0xe7, 0x0d, 0xf8, 0x3a, 0x6d, + 0xdd, 0x30, 0x57, 0xec, 0x05, 0x70, 0xef, 0x80, 0x8c, 0x8a, + 0xe1, 0x2e, 0x9e, 0xd4, 0xd1, 0xb4, 0xdc, 0x14, 0x87, 0xf5, + 0x97, 0x4b, 0x2c, 0xd9, 0x93, 0x71, 0x3a, 0x15, 0x25, 0xa0, + 0x8a, 0xb2, 0x93, 0xb5, 0x62, 0xf7, 0x81, 0x09, 0x9c, 0x8d, + 0xe5, 0x09, 0xb2, 0xda, 0x07, 0xf2, 0x33, 0xf6, 0x5c, 0xd2, + 0x71, 0xb1, 0xaf, 0xea, 0x62, 0x05, 0xf3, 0x5f, 0xd9, 0xb1, + 0x8e, 0xd1, 0x5d, 0xf9, 0x89, 0x85, 0x78, 0xd5, 0x54, 0x56, + 0x6c, 0xd4, 0x20, 0x45, 0xe7, 0x03, 0xcc, 0x25, 0x7e, 0xde, + 0x40, 0x3d, 0xb2, 0x46, 0x90, 0x59, 0xc2, 0xfc, 0xdc, 0x15, + 0xd4, 0x80, 0x5b, 0xd8, 0xec, 0x2e, 0x49, 0xd7, 0xd8, 0x45, + 0x0e, 0x7d, 0xba, 0x09, 0x32, 0x60, 0xdb, 0x64, 0xd6, 0x44, + 0xd7, 0xea, 0xb4, 0xb1, 0xb6, 0xfc, 0x1b, 0xde, 0x4a, 0x48, + 0xa1, 0x6c, 0x5b, 0x5a, 0x1f, 0x35, 0x55, 0x18, 0xd2, 0xae, + 0x88, 0x46, 0x5f, 0x2d, 0x25, 0xb0, 0xd9, 0x4f, 0x40, 0xbb, + 0x23, 0x40, 0x9f, 0x85, 0x19, 0x4e, 0xd4, 0xfc, 0xcd, 0xc4, + 0x59, 0x98, 0x27, 0xe2, 0x7e, 0xab, 0x36, 0x52, 0x51, 0x30, + 0xf4, 0xb4, 0xf3, 0xa1, 0x8d, 0x87, 0x01, 0xe5, 0x2d, 0x42, + 0x4b, 0x87, 0x70, 0x95, 0xde, 0x9d, 0x80, 0x36, 0x4e, 0x8a, + 0x14, 0x40, 0x1c, 0x27, 0x89, 0xe1, 0x42, 0xee, 0x77, 0x87, + 0x4f, 0x4d, 0xb0, 0x38, 0x3f, 0x05, 0x67, 0xe3, 0x9c, 0x88, + 0xd8, 0x68, 0x75, 0xf3, 0xf9, 0x92, 0x8e, 0x78, 0xbe, 0x43, + 0xb9, 0x28, 0xa9, 0x2e, 0xde, 0xc9, 0xbd, 0x48, 0x99, 0x4d, + 0x7d, 0xb6, 0xba, 0x3b, 0x31, 0x20, 0x9f, 0x0c, 0xc4, 0x90, + 0xae, 0x4a, 0x33, 0x17, 0x58, 0x13, 0xb5, 0xfd, 0xec, 0xef, + 0x87, 0x21, 0x0c, 0xce, 0xbd, 0xbd, 0x85, 0x61, 0x07, 0xb8, + 0x7f, 0x93, 0xaa, 0xbd, 0xc6, 0xae, 0x63, 0x2c, 0x12, 0x6a, + 0xaa, 0xe2, 0x9d, 0xf8, 0x55, 0xac, 0xc3, 0xa3, 0x40, 0xf8, + 0xad, 0xfb, 0x21, 0x89, 0x5c, 0x90, 0x21, 0x40, 0x57, 0xfb, + 0x08, 0x5d, 0x28, 0x83, 0xcc, 0x46, 0x76, 0x44, 0x2e, 0xe4, + 0x5b, 0xe7, 0x37, 0x79, 0xc7, 0xc8, 0x0a, 0x0b, 0xc2, 0xb6, + 0x17, 0x48, 0x8c, 0x2b, 0x67, 0xd6, 0xf9, 0x33, 0xe0, 0xbd, + 0x6b, 0xea, 0x79, 0x7d, 0x6d, 0x5b, 0x23, 0x7e, 0x5e, 0x5d, + 0x53, 0xcf, 0x01, 0xea, 0x86, 0x5f, 0x8a, 0x4b, 0x1f, 0x32, + 0xcd, 0xdb, 0x30, 0xe3, 0x27, 0x3f, 0x10, 0x4b, 0xba, 0x3c, + 0xf9, 0x42, 0x78, 0x52, 0x01, 0x87, 0xed, 0xf4, 0x15, 0xa4, + 0x8d, 0xa0, 0x1d, 0xc7, 0x89, 0xf5, 0x4f, 0x99, 0x28, 0x04, + 0xa7, 0x87, 0xf8, 0x9b, 0x77, 0x18, 0x0a, 0xb8, 0x59, 0xf3, + 0xcd, 0x74, 0xf1, 0xd5, 0x13, 0x61, 0xca, 0x02, 0x95, 0xe6, + 0x50, 0x30, 0x96, 0x83, 0x35, 0xf8, 0xd2, 0xea, 0xd7, 0xd6, + 0x5f, 0xbf, 0xab, 0x9b, 0x91, 0xc9, 0xc4, 0x93, 0x57, 0xb5, + 0x55, 0xb6, 0x83, 0xb0, 0x83, 0x17, 0x78, 0xc9, 0xb6, 0xa5, + 0x55, 0x9b, 0x2d, 0x08, 0x1f, 0x09, 0x37, 0x1c, 0x0a, 0x3a, + 0xdc, 0x32, 0xeb, 0x20 + }; + + const uint8_t *key = &static_keys_a[(nonce[0] >> 3) * 0x70]; + for(size_t i=0; i> 3) * 0x70]; + for(size_t i=0; ibright, rpcrypt->ambassador, nonce, morning); + const uint8_t keys_1[] = { + 0xc8, 0x48, 0xc2, 0xb4, 0x08, 0xeb, 0x88, 0xf7, 0x5f, 0x4a, + 0x09, 0x2d, 0x59, 0x1f, 0x09, 0xcd, 0x1c, 0x18, 0xf4, 0x7a, + 0x28, 0x4a, 0x96, 0x6d, 0xb3, 0x59, 0x71, 0x53, 0x75, 0x7e, + 0x82, 0x50, 0x57, 0xe4, 0x59, 0xb3, 0xf4, 0x49, 0x69, 0x40, + 0xeb, 0x17, 0xc9, 0x9f, 0x17, 0x97, 0x71, 0xae, 0xc9, 0x60, + 0x7f, 0xf8, 0x2e, 0x08, 0x94, 0xe8, 0x43, 0xea, 0xda, 0x6b, + 0xe5, 0x19, 0x59, 0x33, 0x1f, 0x89, 0xae, 0x47, 0x57, 0x7b, + 0x1c, 0x66, 0xfe, 0xff, 0x95, 0xbf, 0x55, 0x6b, 0xd5, 0x93, + 0x27, 0xea, 0xa6, 0x24, 0x67, 0x39, 0x9f, 0xd3, 0x0c, 0xaa, + 0x26, 0x42, 0xe7, 0x66, 0x4d, 0xd8, 0x18, 0x75, 0xfe, 0x44, + 0x03, 0x46, 0xee, 0x3e, 0xf8, 0x3c, 0xb7, 0x85, 0x97, 0x03, + 0x07, 0x06, 0x92, 0xff, 0x59, 0x17, 0x27, 0x0b, 0x21, 0xf7, + 0x05, 0x7f, 0x69, 0x90, 0x0e, 0x38, 0x91, 0xc6, 0x67, 0x23, + 0x48, 0xba, 0x08, 0x8e, 0x57, 0xdd, 0x91, 0xd0, 0x40, 0x47, + 0x1c, 0x5b, 0xbf, 0xc8, 0x06, 0x3f, 0x96, 0xa0, 0xdc, 0x00, + 0xe5, 0x9a, 0xf5, 0x3b, 0x90, 0x80, 0x66, 0xbb, 0x0f, 0x93, + 0x30, 0x07, 0x2b, 0x56, 0x45, 0xa0, 0x9a, 0xb1, 0xb0, 0x72, + 0x0c, 0xe4, 0xdd, 0x70, 0xdd, 0x7c, 0x5a, 0xbf, 0xd4, 0xe8, + 0x0d, 0xca, 0x37, 0xe5, 0x0e, 0xfd, 0x12, 0xee, 0x79, 0x9a, + 0x5e, 0xa7, 0x1e, 0x31, 0xaf, 0x1f, 0x46, 0x52, 0xca, 0xf3, + 0x42, 0x00, 0x3d, 0xf2, 0x89, 0x7c, 0x1c, 0x77, 0x60, 0xb7, + 0x4a, 0x80, 0x76, 0x47, 0x4b, 0x3f, 0xb6, 0x91, 0x2f, 0x9c, + 0xc2, 0xf1, 0xad, 0x44, 0x29, 0xcb, 0x32, 0x8c, 0x0a, 0x8d, + 0x05, 0x75, 0x46, 0xa1, 0xf8, 0xda, 0x1a, 0xa7, 0x20, 0xde, + 0x32, 0x59, 0xfe, 0x70, 0xb5, 0x87, 0xf3, 0x92, 0xfd, 0xb4, + 0xdf, 0xf4, 0xa6, 0xe3, 0x7d, 0x98, 0x3b, 0xe1, 0xba, 0x18, + 0xdb, 0x61, 0xd1, 0xc2, 0xa6, 0xee, 0x08, 0x25, 0xfa, 0x86, + 0x8a, 0x7b, 0xfe, 0xbc, 0x02, 0xbd, 0x22, 0x5f, 0x25, 0x30, + 0x51, 0x5d, 0x28, 0x36, 0x5a, 0x29, 0x8e, 0x52, 0xeb, 0x49, + 0x14, 0x28, 0x7f, 0x0a, 0xc4, 0x69, 0x25, 0x85, 0x6b, 0xec, + 0x33, 0x33, 0x57, 0xab, 0x50, 0x13, 0xcf, 0xe7, 0x73, 0x78, + 0x23, 0x06, 0x4d, 0x1f, 0xbb, 0x38, 0x11, 0xb1, 0x6e, 0x6c, + 0x6f, 0xcd, 0x4b, 0x0e, 0x42, 0x85, 0x80, 0xbb, 0xa8, 0x39, + 0x68, 0xc9, 0x5b, 0xbb, 0x46, 0x58, 0x86, 0x88, 0x57, 0x88, + 0x5e, 0xea, 0x7c, 0x37, 0xdf, 0xfd, 0x02, 0x39, 0x45, 0x89, + 0x2e, 0xf2, 0xe4, 0xf0, 0x08, 0xc0, 0xb6, 0xeb, 0x9c, 0x6e, + 0x7a, 0x81, 0xa3, 0x26, 0x46, 0xfe, 0xe1, 0x70, 0x3c, 0x3e, + 0x11, 0x7a, 0x32, 0xce, 0x45, 0x02, 0x7d, 0x32, 0xcd, 0x08, + 0x07, 0x06, 0xa3, 0xa8, 0xf7, 0x34, 0xfe, 0xee, 0x06, 0xb0, + 0x14, 0xd6, 0x6b, 0x2d, 0x2e, 0x01, 0xaf, 0x77, 0x11, 0xec, + 0x1f, 0x31, 0x38, 0x17, 0x9c, 0xd0, 0xe0, 0xc5, 0x4d, 0xa4, + 0xd6, 0xad, 0xb9, 0xe6, 0xe1, 0xe3, 0xe2, 0x9e, 0x44, 0x91, + 0x9a, 0x5e, 0x26, 0xca, 0xcc, 0xda, 0x4d, 0xd7, 0x78, 0x6a, + 0x75, 0xa6, 0x19, 0xad, 0xcc, 0x62, 0xc7, 0xb6, 0x0d, 0x14, + 0xb1, 0xbe, 0xeb, 0xcb, 0x10, 0xcf, 0xa9, 0xee, 0xe2, 0x42, + 0x08, 0x35, 0x8a, 0x5c, 0xbc, 0xf1, 0x49, 0xfe, 0x64, 0x78, + 0x03, 0x49, 0x0c, 0x85, 0xf0, 0xe4, 0x77, 0x26, 0xd2, 0x5e, + 0xf5, 0xc1, 0x3b, 0x3d, 0x2d, 0xcc, 0xcf, 0x2a, 0xac, 0xed, + 0x88, 0x52, 0x74, 0x6d, 0xbf, 0xb2, 0xb9, 0xf7, 0x58, 0x51, + 0x1c, 0x50, 0xf6, 0x3d, 0xf2, 0xc4, 0x47, 0x0a, 0x21, 0x30, + 0x47, 0x81, 0x2d, 0xe4, 0x75, 0x0d, 0x8f, 0x2d, 0x22, 0xb0, + 0x63, 0x27 + }; + + for(size_t i=0; itarget = target; + chiaki_rpcrypt_bright_ambassador(target, rpcrypt->bright, rpcrypt->ambassador, nonce, morning); +} + +CHIAKI_EXPORT void chiaki_rpcrypt_init_regist_ps4_pre10(ChiakiRPCrypt *rpcrypt, const uint8_t *ambassador, uint32_t pin) +{ + rpcrypt->target = CHIAKI_TARGET_PS4_9; // representative, might not be the actual version + static const uint8_t regist_aes_key[CHIAKI_RPCRYPT_KEY_SIZE] = + { 0x3f, 0x1c, 0xc4, 0xb6, 0xdc, 0xbb, 0x3e, 0xcc, 0x50, 0xba, 0xed, 0xef, 0x97, 0x34, 0xc7, 0xc9 }; memcpy(rpcrypt->ambassador, ambassador, sizeof(rpcrypt->ambassador)); memcpy(rpcrypt->bright, regist_aes_key, sizeof(rpcrypt->bright)); rpcrypt->bright[0] ^= (uint8_t)((pin >> 0x18) & 0xff); @@ -83,10 +911,101 @@ CHIAKI_EXPORT void chiaki_rpcrypt_init_regist(ChiakiRPCrypt *rpcrypt, const uint rpcrypt->bright[3] ^= (uint8_t)((pin >> 0x00) & 0xff); } +CHIAKI_EXPORT ChiakiErrorCode chiaki_rpcrypt_init_regist(ChiakiRPCrypt *rpcrypt, const uint8_t *ambassador, size_t key_0_off, uint32_t pin) +{ + static const uint8_t keys_0[] = { + 0xbe, 0xce, 0x5d, 0xf0, 0xc1, 0x7d, 0xb5, 0xd0, 0xcb, 0x30, + 0x13, 0x5d, 0xaa, 0x56, 0x23, 0xfb, 0xc4, 0xbc, 0xf1, 0x8f, + 0x38, 0x57, 0xfb, 0xd4, 0xd4, 0x3f, 0x26, 0x38, 0xb5, 0xce, + 0xed, 0x6a, 0x21, 0xbc, 0x38, 0xd0, 0x1e, 0x68, 0xcc, 0x7b, + 0x45, 0xd1, 0xbe, 0x42, 0x1a, 0x08, 0xaa, 0x16, 0xfd, 0xb0, + 0xc0, 0xf4, 0xda, 0x35, 0xe9, 0x12, 0xfd, 0x21, 0x07, 0x48, + 0x34, 0xc1, 0xfc, 0x9f, 0x8c, 0xb6, 0xcb, 0x5d, 0xb2, 0x9c, + 0x84, 0xe0, 0x1a, 0xfa, 0xa0, 0xc7, 0xeb, 0x3a, 0x93, 0xb3, + 0xb3, 0xf1, 0x15, 0xaf, 0x13, 0xbd, 0x21, 0xab, 0xea, 0x5b, + 0x80, 0x50, 0x6b, 0x31, 0x1d, 0x7c, 0x1d, 0x40, 0xba, 0x3c, + 0x56, 0x0e, 0xe7, 0x94, 0x3a, 0x5b, 0xa1, 0x40, 0x80, 0x74, + 0x0a, 0xad, 0x28, 0xcf, 0x47, 0xdf, 0x42, 0xa6, 0x69, 0xe9, + 0x5e, 0xbb, 0xc0, 0xc0, 0x0e, 0xb2, 0xc5, 0x8a, 0xee, 0x08, + 0x03, 0xd2, 0x84, 0xe5, 0x91, 0x00, 0x1d, 0x46, 0x06, 0x55, + 0x09, 0x9d, 0x39, 0x9f, 0xd8, 0xe7, 0xfd, 0xad, 0x9e, 0x93, + 0x97, 0xc5, 0xea, 0xe7, 0xa3, 0x10, 0xa7, 0xf2, 0xa2, 0x93, + 0x7f, 0x07, 0x04, 0xb4, 0xee, 0xbb, 0xbf, 0x88, 0x23, 0x9c, + 0x6e, 0xa7, 0x62, 0xb1, 0x4b, 0x67, 0x1e, 0xb8, 0x3b, 0x1f, + 0x64, 0x93, 0x5a, 0x99, 0xec, 0xda, 0xfd, 0x0c, 0x6a, 0xb7, + 0xfe, 0xe4, 0x12, 0x76, 0x32, 0x65, 0xb8, 0x41, 0x23, 0xd1, + 0x17, 0x09, 0x9c, 0x24, 0x2d, 0x5c, 0x9d, 0x12, 0x79, 0xde, + 0xa1, 0xce, 0x69, 0xac, 0xa4, 0xbc, 0x39, 0x2f, 0x57, 0x38, + 0x84, 0x61, 0x2d, 0x2a, 0xe8, 0x04, 0xf8, 0xd5, 0x9d, 0x0b, + 0xff, 0x7e, 0x56, 0x0c, 0xec, 0x87, 0x0a, 0x1e, 0xab, 0xdf, + 0x93, 0x81, 0x13, 0xee, 0xcf, 0x32, 0x02, 0x5a, 0xbf, 0xb0, + 0x17, 0xb7, 0xba, 0xb5, 0x7f, 0xf0, 0x01, 0x7b, 0xe1, 0xcb, + 0x39, 0x7e, 0x60, 0x6d, 0xa4, 0x75, 0x6e, 0x29, 0x92, 0x45, + 0xa6, 0x4f, 0x74, 0x00, 0x86, 0x78, 0x73, 0xbe, 0xfd, 0x3e, + 0xe0, 0xd1, 0x0c, 0x6c, 0x0b, 0x49, 0x09, 0x83, 0x6c, 0x85, + 0x8a, 0x1d, 0xcb, 0x16, 0xce, 0x81, 0x7c, 0x49, 0xc9, 0x2c, + 0x63, 0x61, 0xde, 0xe2, 0x3f, 0x98, 0xb2, 0x73, 0xf0, 0x9a, + 0xec, 0x7b, 0x7c, 0xf1, 0xc9, 0xe1, 0x7f, 0xa5, 0x19, 0x8b, + 0x4b, 0xe8, 0x38, 0xa4, 0x34, 0x7d, 0xf4, 0x28, 0xfe, 0x0d, + 0x4d, 0x11, 0x57, 0x0c, 0x95, 0xf1, 0xaf, 0xd7, 0x34, 0x80, + 0xf4, 0xeb, 0x9b, 0x50, 0xe6, 0x6a, 0x5d, 0xea, 0xce, 0x0c, + 0x85, 0x4e, 0xc5, 0x5b, 0x93, 0x44, 0xc4, 0x24, 0x98, 0x80, + 0xfc, 0xf7, 0x72, 0x9c, 0x31, 0x0b, 0xee, 0x89, 0x67, 0xb3, + 0xa2, 0x69, 0x4f, 0xb3, 0x79, 0x5a, 0x14, 0x02, 0x70, 0xed, + 0x50, 0x13, 0x75, 0x00, 0x6a, 0xf3, 0xc6, 0x05, 0x1a, 0x00, + 0x33, 0x34, 0xf5, 0xac, 0x9e, 0x04, 0xdb, 0xc2, 0x00, 0xb0, + 0x1b, 0xc4, 0xf3, 0x97, 0x9d, 0x7f, 0xbe, 0xb8, 0x23, 0x8d, + 0x99, 0xe7, 0xcb, 0x74, 0x37, 0x4c, 0x57, 0xec, 0xd2, 0x69, + 0x49, 0x46, 0x75, 0x74, 0xaf, 0x51, 0x40, 0xa4, 0x11, 0x7b, + 0xb3, 0x2f, 0x51, 0xda, 0xe2, 0xef, 0x33, 0x73, 0x12, 0x18, + 0x25, 0x39, 0x03, 0x09, 0xca, 0x49, 0xdc, 0x8e, 0xf1, 0x94, + 0xd7, 0x80, 0x17, 0x9e, 0x87, 0x46, 0xc1, 0x04, 0x78, 0xd1, + 0xe5, 0x3d, 0x25, 0x88, 0xec, 0x72, 0x3a, 0x28, 0x41, 0x68, + 0x14, 0x6e, 0x10, 0xe4, 0xc9, 0x57, 0x75, 0x90, 0xfe, 0x22, + 0x1a, 0x63, 0x8e, 0xf4, 0xb8, 0x8d, 0x1a, 0x36, 0xfd, 0xb6, + 0xcb, 0x72, 0xc2, 0x97, 0x52, 0x9f, 0x91, 0x72, 0x1b, 0x75, + 0x57, 0x90, 0x3b, 0xfd, 0x5a, 0x93, 0x8c, 0xdb, 0xfc, 0xa3, + 0x03, 0xdf + }; + + if(key_0_off >= 0x20) + return CHIAKI_ERR_INVALID_DATA; + + rpcrypt->target = CHIAKI_TARGET_PS4_10; // representative, might not be the actual version + + memcpy(rpcrypt->ambassador, ambassador, sizeof(rpcrypt->ambassador)); + + for(size_t i=0; ibright[i] = keys_0[i*0x20 + key_0_off]; + rpcrypt->bright[0xc] ^= (uint8_t)((pin >> 0x18) & 0xff); + rpcrypt->bright[0xd] ^= (uint8_t)((pin >> 0x10) & 0xff); + rpcrypt->bright[0xe] ^= (uint8_t)((pin >> 0x08) & 0xff); + rpcrypt->bright[0xf] ^= (uint8_t)((pin >> 0x00) & 0xff); + + return CHIAKI_ERR_SUCCESS; +} + +#define HMAC_KEY_SIZE 0x10 +static const uint8_t hmac_key[HMAC_KEY_SIZE] = { 0x20, 0xd6, 0x6f, 0x59, 0x04, 0xea, 0x7c, 0x14, 0xe5, 0x57, 0xff, 0xc5, 0x2e, 0x48, 0x8a, 0xc8 }; +static const uint8_t hmac_key_ps4_pre10[HMAC_KEY_SIZE] = { 0xac, 0x07, 0x88, 0x83, 0xc8, 0x3a, 0x1f, 0xe8, 0x11, 0x46, 0x3a, 0xf3, 0x9e, 0xe3, 0xe3, 0x77 }; + +static const uint8_t *rpcrypt_hmac_key(ChiakiRPCrypt *rpcrypt) +{ + switch(rpcrypt->target) + { + case CHIAKI_TARGET_PS4_8: + case CHIAKI_TARGET_PS4_9: + return hmac_key_ps4_pre10; + default: + return hmac_key; + } +} + + #ifdef CHIAKI_LIB_ENABLE_MBEDTLS CHIAKI_EXPORT ChiakiErrorCode chiaki_rpcrypt_generate_iv(ChiakiRPCrypt *rpcrypt, uint8_t *iv, uint64_t counter) { - uint8_t hmac_key[] = { 0xac, 0x07, 0x88, 0x83, 0xc8, 0x3a, 0x1f, 0xe8, 0x11, 0x46, 0x3a, 0xf3, 0x9e, 0xe3, 0xe3, 0x77 }; + const uint8_t *hmac_key = rpcrypt_hmac_key(rpcrypt); uint8_t buf[CHIAKI_RPCRYPT_KEY_SIZE + 8]; memcpy(buf, rpcrypt->ambassador, CHIAKI_RPCRYPT_KEY_SIZE); @@ -114,7 +1033,7 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_rpcrypt_generate_iv(ChiakiRPCrypt *rpcrypt, } while(0) // https://tls.mbed.org/module-level-design-hashing GOTO_ERROR(mbedtls_md_setup(&ctx, mbedtls_md_info_from_type(type) , 1)); - GOTO_ERROR(mbedtls_md_hmac_starts(&ctx, hmac_key, sizeof(hmac_key))); + GOTO_ERROR(mbedtls_md_hmac_starts(&ctx, hmac_key, HMAC_KEY_SIZE)); GOTO_ERROR(mbedtls_md_hmac_update(&ctx, (const unsigned char *) buf, sizeof(buf))); GOTO_ERROR(mbedtls_md_hmac_finish(&ctx, hmac)); #undef GOTO_ERROR @@ -172,7 +1091,7 @@ error: #else CHIAKI_EXPORT ChiakiErrorCode chiaki_rpcrypt_generate_iv(ChiakiRPCrypt *rpcrypt, uint8_t *iv, uint64_t counter) { - uint8_t hmac_key[] = { 0xac, 0x07, 0x88, 0x83, 0xc8, 0x3a, 0x1f, 0xe8, 0x11, 0x46, 0x3a, 0xf3, 0x9e, 0xe3, 0xe3, 0x77 }; + const uint8_t *hmac_key = rpcrypt_hmac_key(rpcrypt); uint8_t buf[CHIAKI_RPCRYPT_KEY_SIZE + 8]; memcpy(buf, rpcrypt->ambassador, CHIAKI_RPCRYPT_KEY_SIZE); diff --git a/lib/src/session.c b/lib/src/session.c index db86c70..caf8236 100644 --- a/lib/src/session.c +++ b/lib/src/session.c @@ -46,7 +46,7 @@ #define SESSION_EXPECT_TIMEOUT_MS 5000 static void *session_thread_func(void *arg); -static bool session_thread_request_session(ChiakiSession *session, ChiakiRpVersion *server_version_out); +static bool session_thread_request_session(ChiakiSession *session, ChiakiTarget *target_out); const char *chiaki_rp_application_reason_string(uint32_t reason) { @@ -67,26 +67,30 @@ const char *chiaki_rp_application_reason_string(uint32_t reason) } } -const char *chiaki_rp_version_string(ChiakiRpVersion version) +const char *chiaki_rp_version_string(ChiakiTarget version) { switch(version) { - case CHIAKI_RP_VERSION_8_0: + case CHIAKI_TARGET_PS4_8: return "8.0"; - case CHIAKI_RP_VERSION_9_0: + case CHIAKI_TARGET_PS4_9: return "9.0"; + case CHIAKI_TARGET_PS4_10: + return "10.0"; default: return NULL; } } -CHIAKI_EXPORT ChiakiRpVersion chiaki_rp_version_parse(const char *rp_version_str) +CHIAKI_EXPORT ChiakiTarget chiaki_rp_version_parse(const char *rp_version_str) { if(strcmp(rp_version_str, "8.0") == 0) - return CHIAKI_RP_VERSION_8_0; + return CHIAKI_TARGET_PS4_8; if(strcmp(rp_version_str, "9.0") == 0) - return CHIAKI_RP_VERSION_9_0; - return CHIAKI_RP_VERSION_UNKNOWN; + return CHIAKI_TARGET_PS4_9; + if(strcmp(rp_version_str, "10.0") == 0) + return CHIAKI_TARGET_PS4_10; + return CHIAKI_TARGET_PS4_UNKNOWN; } CHIAKI_EXPORT void chiaki_connect_video_profile_preset(ChiakiConnectVideoProfile *profile, ChiakiVideoResolutionPreset resolution, ChiakiVideoFPSPreset fps) @@ -174,7 +178,7 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_session_init(ChiakiSession *session, Chiaki session->log = log; session->quit_reason = CHIAKI_QUIT_REASON_NONE; - session->rp_version = CHIAKI_RP_VERSION_9_0; + session->target = CHIAKI_TARGET_PS4_10; ChiakiErrorCode err = chiaki_cond_init(&session->state_cond); if(err != CHIAKI_ERR_SUCCESS) @@ -369,13 +373,20 @@ static void *session_thread_func(void *arg) CHIAKI_LOGI(session->log, "Starting session request"); - ChiakiRpVersion server_rp_version = CHIAKI_RP_VERSION_UNKNOWN; - success = session_thread_request_session(session, &server_rp_version); + ChiakiTarget server_target = CHIAKI_TARGET_PS4_UNKNOWN; + success = session_thread_request_session(session, &server_target); - if(!success && server_rp_version != CHIAKI_RP_VERSION_UNKNOWN) + if(!success && server_target != CHIAKI_TARGET_PS4_UNKNOWN) { CHIAKI_LOGI(session->log, "Attempting to re-request session with Server's RP-Version"); - session->rp_version = server_rp_version; + session->target = server_target; + success = session_thread_request_session(session, &server_target); + } + + if(!success && server_target != CHIAKI_TARGET_PS4_UNKNOWN) + { + CHIAKI_LOGI(session->log, "Attempting to re-request session even harder with Server's RP-Version!!!"); + session->target = server_target; success = session_thread_request_session(session, NULL); } @@ -384,7 +395,7 @@ static void *session_thread_func(void *arg) CHIAKI_LOGI(session->log, "Session request successful"); - chiaki_rpcrypt_init_auth(&session->rpcrypt, session->nonce, session->connect_info.morning); + chiaki_rpcrypt_init_auth(&session->rpcrypt, session->target, session->nonce, session->connect_info.morning); // PS4 doesn't always react right away, sleep a bit chiaki_cond_timedwait_pred(&session->state_cond, &session->state_mutex, 10, session_check_state_pred, session); @@ -584,9 +595,9 @@ static void parse_session_response(SessionResponse *response, ChiakiHttpResponse /** - * @param server_version_out if NULL, version mismatch means to fail the entire session, otherwise report the version here + * @param target_out if NULL, version mismatch means to fail the entire session, otherwise report the target here */ -static bool session_thread_request_session(ChiakiSession *session, ChiakiRpVersion *server_version_out) +static bool session_thread_request_session(ChiakiSession *session, ChiakiTarget *target_out) { chiaki_socket_t session_sock = CHIAKI_INVALID_SOCKET; for(struct addrinfo *ai=session->connect_info.host_addrinfos; ai; ai=ai->ai_next) @@ -676,7 +687,7 @@ static bool session_thread_request_session(ChiakiSession *session, ChiakiRpVersi CHIAKI_LOGI(session->log, "Connected to %s:%d", session->connect_info.hostname, SESSION_PORT); static const char session_request_fmt[] = - "GET /sce/rp/session HTTP/1.1\r\n" + "GET %s HTTP/1.1\r\n" "Host: %s:%d\r\n" "User-Agent: remoteplay Windows\r\n" "Connection: close\r\n" @@ -684,6 +695,9 @@ static bool session_thread_request_session(ChiakiSession *session, ChiakiRpVersi "RP-Registkey: %s\r\n" "Rp-Version: %s\r\n" "\r\n"; + const char *path = (session->target == CHIAKI_TARGET_PS4_8 || session->target == CHIAKI_TARGET_PS4_9) + ? "/sce/rp/session" + : "/sie/ps4/rp/sess/init"; size_t regist_key_len = sizeof(session->connect_info.regist_key); for(size_t i=0; irp_version); + const char *rp_version_str = chiaki_rp_version_string(session->target); char buf[512]; int request_len = snprintf(buf, sizeof(buf), session_request_fmt, - session->connect_info.hostname, SESSION_PORT, regist_key_hex, rp_version_str ? rp_version_str : ""); + path, session->connect_info.hostname, SESSION_PORT, regist_key_hex, rp_version_str ? rp_version_str : ""); if(request_len < 0 || request_len >= sizeof(buf)) { CHIAKI_SOCKET_CLOSE(session_sock); @@ -774,12 +788,20 @@ static bool session_thread_request_session(ChiakiSession *session, ChiakiRpVersi session->quit_reason = CHIAKI_QUIT_REASON_SESSION_REQUEST_UNKNOWN; } } - else if(response.error_code == CHIAKI_RP_APPLICATION_REASON_RP_VERSION && server_version_out && response.rp_version) + else if((response.error_code == CHIAKI_RP_APPLICATION_REASON_RP_VERSION + || response.error_code == CHIAKI_RP_APPLICATION_REASON_UNKNOWN) + && target_out && response.rp_version && strcmp(rp_version_str, response.rp_version)) { - CHIAKI_LOGI(session->log, "Reported RP-Version mismatch. ours = %s, server = %s", rp_version_str ? rp_version_str : "", response.rp_version); - *server_version_out = chiaki_rp_version_parse(response.rp_version); - if(*server_version_out != CHIAKI_RP_VERSION_UNKNOWN) - CHIAKI_LOGI(session->log, "Detected Server RP-Version %s", chiaki_rp_version_string(*server_version_out)); + CHIAKI_LOGI(session->log, "Reported RP-Version mismatch. ours = %s, server = %s", + rp_version_str ? rp_version_str : "", response.rp_version); + *target_out = chiaki_rp_version_parse(response.rp_version); + if(*target_out != CHIAKI_TARGET_PS4_UNKNOWN) + CHIAKI_LOGI(session->log, "Detected Server RP-Version %s", chiaki_rp_version_string(*target_out)); + else if(!strcmp(response.rp_version, "5.0")) + { + CHIAKI_LOGI(session->log, "Reported Server RP-Version is 5.0. This is probably nonsense, let's try with 9.0"); + *target_out = CHIAKI_TARGET_PS4_9; + } else { CHIAKI_LOGE(session->log, "Server RP-Version is unknown"); diff --git a/test/regist.c b/test/regist.c index 866eeb8..d2a4aee 100644 --- a/test/regist.c +++ b/test/regist.c @@ -24,25 +24,25 @@ static const uint8_t ambassador[CHIAKI_RPCRYPT_KEY_SIZE] = { 0x13, 0x37, 0xde, 0 static const uint32_t pin = 13374201; static const char * const psn_id = "ChiakiNanami1337"; -static MunitResult test_aeropause(const MunitParameter params[], void *user) +static MunitResult test_aeropause_ps4_pre10(const MunitParameter params[], void *user) { uint8_t expected[CHIAKI_RPCRYPT_KEY_SIZE] = { 0x0b, 0xe1, 0x2f, 0xbb, 0x4c, 0x7c, 0x99, 0x4a, 0x41, 0x1e, 0x2d, 0x4c, 0xa4, 0x19, 0xf4, 0x35 }; uint8_t aeropause[CHIAKI_RPCRYPT_KEY_SIZE]; - chiaki_rpcrypt_aeropause(aeropause, ambassador); + chiaki_rpcrypt_aeropause_ps4_pre10(aeropause, ambassador); munit_assert_memory_equal(sizeof(expected), aeropause, expected); return MUNIT_OK; } -static MunitResult test_pin_bright(const MunitParameter params[], void *user) +static MunitResult test_pin_bright_ps4_pre10(const MunitParameter params[], void *user) { uint8_t expected[CHIAKI_RPCRYPT_KEY_SIZE] = { 0x3f, 0xd0, 0xd6, 0x4f, 0xdc, 0xbb, 0x3e, 0xcc, 0x50, 0xba, 0xed, 0xef, 0x97, 0x34, 0xc7, 0xc9 }; ChiakiRPCrypt rpcrypt; - chiaki_rpcrypt_init_regist(&rpcrypt, ambassador, pin); + chiaki_rpcrypt_init_regist_ps4_pre10(&rpcrypt, ambassador, pin); munit_assert_memory_equal(sizeof(expected), rpcrypt.bright, expected); return MUNIT_OK; } -static MunitResult test_request_payload(const MunitParameter params[], void *user) +static MunitResult test_request_payload_ps4_pre10(const MunitParameter params[], void *user) { uint8_t expected[] = { 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, @@ -65,11 +65,10 @@ static MunitResult test_request_payload(const MunitParameter params[], void *use }; ChiakiRPCrypt rpcrypt; - chiaki_rpcrypt_init_regist(&rpcrypt, ambassador, pin); uint8_t payload[0x400]; size_t payload_size = sizeof(payload); - ChiakiErrorCode err = chiaki_regist_request_payload_format(payload, &payload_size, &rpcrypt, psn_id, NULL); + ChiakiErrorCode err = chiaki_regist_request_payload_format(CHIAKI_TARGET_PS4_9, ambassador, payload, &payload_size, &rpcrypt, psn_id, NULL, pin); munit_assert_int(err, ==, CHIAKI_ERR_SUCCESS); munit_assert_size(payload_size, ==, sizeof(expected)); munit_assert_memory_equal(sizeof(expected), payload, expected); @@ -78,28 +77,28 @@ static MunitResult test_request_payload(const MunitParameter params[], void *use MunitTest tests_regist[] = { { - "/aeropause", - test_aeropause, + "/aeropause_ps4_pre10", + test_aeropause_ps4_pre10, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL }, { - "/pin_bright", - test_pin_bright, + "/pin_bright_ps4_pre10", + test_pin_bright_ps4_pre10, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL }, { - "/request_payload", - test_request_payload, + "/request_payload_ps4_pre10", + test_request_payload_ps4_pre10, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL }, { NULL, NULL, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL } -}; \ No newline at end of file +}; diff --git a/test/rpcrypt.c b/test/rpcrypt.c index e6e7f12..8297d37 100644 --- a/test/rpcrypt.c +++ b/test/rpcrypt.c @@ -19,19 +19,16 @@ #include - -static const uint8_t nonce[] = { 0x43, 0x9, 0x67, 0xae, 0x36, 0x4b, 0x1c, 0x45, 0x26, 0x62, 0x37, 0x7a, 0xbf, 0x3f, 0xe9, 0x39 }; -static const uint8_t morning[] = { 0xd2, 0x78, 0x9f, 0x51, 0x85, 0xa7, 0x99, 0xa2, 0x44, 0x52, 0x77, 0x9c, 0x2b, 0x83, 0xcf, 0x7 }; - - -static MunitResult test_bright_ambassador(const MunitParameter params[], void *user) +static MunitResult test_bright_ambassador_ps4_pre10(const MunitParameter params[], void *user) { + static const uint8_t nonce[] = { 0x43, 0x9, 0x67, 0xae, 0x36, 0x4b, 0x1c, 0x45, 0x26, 0x62, 0x37, 0x7a, 0xbf, 0x3f, 0xe9, 0x39 }; + static const uint8_t morning[] = { 0xd2, 0x78, 0x9f, 0x51, 0x85, 0xa7, 0x99, 0xa2, 0x44, 0x52, 0x77, 0x9c, 0x2b, 0x83, 0xcf, 0x7 }; static const uint8_t bright_expected[] = { 0xa4, 0x4e, 0x2a, 0x16, 0x5e, 0x20, 0xd3, 0xf, 0xaa, 0x11, 0x8b, 0xc7, 0x7c, 0xa7, 0xdc, 0x11 }; static const uint8_t ambassador_expected[] = { 0x1d, 0xa8, 0xb9, 0x1f, 0x6e, 0x26, 0x64, 0x2e, 0xbc, 0x8, 0x8b, 0x0, 0x4f, 0x1, 0x5b, 0x52 }; uint8_t bright[CHIAKI_RPCRYPT_KEY_SIZE]; uint8_t ambassador[CHIAKI_RPCRYPT_KEY_SIZE]; - chiaki_rpcrypt_bright_ambassador(bright, ambassador, nonce, morning); + chiaki_rpcrypt_bright_ambassador(CHIAKI_TARGET_PS4_9, bright, ambassador, nonce, morning); munit_assert_memory_equal(CHIAKI_RPCRYPT_KEY_SIZE, bright, bright_expected); munit_assert_memory_equal(CHIAKI_RPCRYPT_KEY_SIZE, ambassador, ambassador_expected); @@ -39,15 +36,34 @@ static MunitResult test_bright_ambassador(const MunitParameter params[], void *u return MUNIT_OK; } -static MunitResult test_iv(const MunitParameter params[], void *user) +static MunitResult test_bright_ambassador(const MunitParameter params[], void *user) { + static const uint8_t nonce_local[] = { 0xAE, 0x92, 0xE7, 0x64, 0x88, 0x26, 0x51, 0xEF, 0x89, 0x01, 0x8C, 0xFA, 0x69, 0x6C, 0x69, 0x38 }; + static const uint8_t morning_local[] = { 0x74, 0xA5, 0x9C, 0x96, 0x93, 0xC2, 0x08, 0x3B, 0xA6, 0xA8, 0x4B, 0xA0, 0x50, 0xFA, 0x8E, 0x5A }; + static const uint8_t ambassador_expected[] = { 0x92, 0xBE, 0xE2, 0x19, 0xB9, 0xD5, 0xB1, 0xAB, 0xC6, 0x49, 0x45, 0x77, 0xA4, 0x21, 0xE9, 0xBD }; + static const uint8_t bright_expected[] = { 0x67, 0x40, 0x8C, 0x5E, 0x65, 0x66, 0x5A, 0xD2, 0x91, 0xA8, 0x32, 0xEB, 0xE2, 0xD9, 0x0A, 0xBB }; + + uint8_t bright[CHIAKI_RPCRYPT_KEY_SIZE]; + uint8_t ambassador[CHIAKI_RPCRYPT_KEY_SIZE]; + chiaki_rpcrypt_bright_ambassador(CHIAKI_TARGET_PS4_10, bright, ambassador, nonce_local, morning_local); + + munit_assert_memory_equal(CHIAKI_RPCRYPT_KEY_SIZE, ambassador, ambassador_expected); + munit_assert_memory_equal(CHIAKI_RPCRYPT_KEY_SIZE, bright, bright_expected); + + return MUNIT_OK; +} + +static MunitResult test_iv_ps4_pre10(const MunitParameter params[], void *user) +{ + static const uint8_t nonce[] = { 0x43, 0x9, 0x67, 0xae, 0x36, 0x4b, 0x1c, 0x45, 0x26, 0x62, 0x37, 0x7a, 0xbf, 0x3f, 0xe9, 0x39 }; + static const uint8_t morning[] = { 0xd2, 0x78, 0x9f, 0x51, 0x85, 0xa7, 0x99, 0xa2, 0x44, 0x52, 0x77, 0x9c, 0x2b, 0x83, 0xcf, 0x7 }; static const uint8_t iv_a_expected[] = { 0x6, 0x29, 0xbe, 0x4, 0xe9, 0x91, 0x1c, 0x48, 0xb4, 0x5c, 0x2, 0x6d, 0xb7, 0xb7, 0x88, 0x46 }; static const uint8_t iv_b_expected[] = { 0x3f, 0xd0, 0x83, 0xa, 0xc7, 0x30, 0xfc, 0x56, 0x75, 0x2d, 0xbe, 0xb8, 0x2c, 0x68, 0xa7, 0x4 }; ChiakiRPCrypt rpcrypt; ChiakiErrorCode err; - chiaki_rpcrypt_init_auth(&rpcrypt, nonce, morning); + chiaki_rpcrypt_init_auth(&rpcrypt, CHIAKI_TARGET_PS4_9, nonce, morning); uint8_t iv[CHIAKI_RPCRYPT_KEY_SIZE]; @@ -74,14 +90,49 @@ static MunitResult test_iv(const MunitParameter params[], void *user) return MUNIT_OK; } -#include - -static MunitResult test_encrypt(const MunitParameter params[], void *user) +static MunitResult test_iv_regist(const MunitParameter params[], void *user) { + static const uint8_t ambassador[] = { 0x3e, 0x7e, 0x7a, 0x82, 0x59, 0x73, 0xad, 0xab, 0x2f, 0x69, 0x43, 0x46, 0xbd, 0x44, 0xda, 0xb5 }; + static const uint8_t iv_expected[] = { 0xac, 0x48, 0x99, 0x77, 0xf9, 0x2a, 0xc5, 0x5b, 0xb9, 0x09, 0x3c, 0x33, 0xb6, 0x11, 0x3c, 0x46 }; + ChiakiRPCrypt rpcrypt; ChiakiErrorCode err; - chiaki_rpcrypt_init_auth(&rpcrypt, nonce, morning); + chiaki_rpcrypt_init_regist(&rpcrypt, ambassador, 0, 0); + + uint8_t iv[CHIAKI_RPCRYPT_KEY_SIZE]; + + err = chiaki_rpcrypt_generate_iv(&rpcrypt, iv, 0); + if(err != CHIAKI_ERR_SUCCESS) + return MUNIT_ERROR; + munit_assert_memory_equal(CHIAKI_RPCRYPT_KEY_SIZE, iv, iv_expected); + + return MUNIT_OK; +} + +static MunitResult test_bright_regist(const MunitParameter params[], void *user) +{ + static const uint8_t ambassador[] = { 0xdc, 0xa1, 0xc8, 0x4d, 0xfe, 0x50, 0xd6, 0x57, 0x22, 0xda, 0x09, 0x65, 0x42, 0x31, 0xe7, 0xc2 }; + static const uint8_t bright_expected[] = { 0xed, 0xfc, 0x1d, 0xc5, 0xa2, 0xfe, 0x2d, 0x7f, 0x09, 0x19, 0x85, 0x75, 0x33, 0x6c, 0x13, 0x16 }; + size_t key_0_off = 0x1e; + + ChiakiRPCrypt rpcrypt; + ChiakiErrorCode err; + + chiaki_rpcrypt_init_regist(&rpcrypt, ambassador, key_0_off, 78703893); + munit_assert_memory_equal(CHIAKI_RPCRYPT_KEY_SIZE, rpcrypt.bright, bright_expected); + + return MUNIT_OK; +} + +static MunitResult test_encrypt_ps4_pre10(const MunitParameter params[], void *user) +{ + static const uint8_t nonce[] = { 0x43, 0x9, 0x67, 0xae, 0x36, 0x4b, 0x1c, 0x45, 0x26, 0x62, 0x37, 0x7a, 0xbf, 0x3f, 0xe9, 0x39 }; + static const uint8_t morning[] = { 0xd2, 0x78, 0x9f, 0x51, 0x85, 0xa7, 0x99, 0xa2, 0x44, 0x52, 0x77, 0x9c, 0x2b, 0x83, 0xcf, 0x7 }; + ChiakiRPCrypt rpcrypt; + ChiakiErrorCode err; + + chiaki_rpcrypt_init_auth(&rpcrypt, CHIAKI_TARGET_PS4_9, nonce, morning); // less than block size uint8_t buf_a[] = { 0x13, 0x37, 0xc0, 0xff, 0xee }; @@ -118,12 +169,14 @@ static MunitResult test_encrypt(const MunitParameter params[], void *user) return MUNIT_OK; } -static MunitResult test_decrypt(const MunitParameter params[], void *user) +static MunitResult test_decrypt_ps4_pre10(const MunitParameter params[], void *user) { + static const uint8_t nonce[] = { 0x43, 0x9, 0x67, 0xae, 0x36, 0x4b, 0x1c, 0x45, 0x26, 0x62, 0x37, 0x7a, 0xbf, 0x3f, 0xe9, 0x39 }; + static const uint8_t morning[] = { 0xd2, 0x78, 0x9f, 0x51, 0x85, 0xa7, 0x99, 0xa2, 0x44, 0x52, 0x77, 0x9c, 0x2b, 0x83, 0xcf, 0x7 }; ChiakiRPCrypt rpcrypt; ChiakiErrorCode err; - chiaki_rpcrypt_init_auth(&rpcrypt, nonce, morning); + chiaki_rpcrypt_init_auth(&rpcrypt, CHIAKI_TARGET_PS4_9, nonce, morning); // less than block size uint8_t buf_a[] = { 0x8d, 0xd2, 0x1d, 0xfb }; @@ -162,6 +215,14 @@ static MunitResult test_decrypt(const MunitParameter params[], void *user) MunitTest tests_rpcrypt[] = { + { + "/bright_ambassador_ps4_pre10", + test_bright_ambassador_ps4_pre10, + NULL, + NULL, + MUNIT_TEST_OPTION_NONE, + NULL + }, { "/bright_ambassador", test_bright_ambassador, @@ -171,28 +232,44 @@ MunitTest tests_rpcrypt[] = { NULL }, { - "/iv", - test_iv, + "/iv_ps4_pre10", + test_iv_ps4_pre10, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL }, { - "/encrypt", - test_encrypt, + "/iv_regist", + test_iv_regist, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL }, { - "/decrypt", - test_decrypt, + "/bright_regist", + test_bright_regist, + NULL, + NULL, + MUNIT_TEST_OPTION_NONE, + NULL + }, + { + "/encrypt_ps4_pre10", + test_encrypt_ps4_pre10, + NULL, + NULL, + MUNIT_TEST_OPTION_NONE, + NULL + }, + { + "/decrypt_ps4_pre10", + test_decrypt_ps4_pre10, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL }, { NULL, NULL, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL } -}; \ No newline at end of file +}; From 702d31eb01d37518e77f5c1be3ea493df9f18323 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Sun, 18 Oct 2020 14:27:38 +0200 Subject: [PATCH 039/237] Version 1.3.0 --- CMakeLists.txt | 4 ++-- android/app/build.gradle | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index f7049a1..d59f031 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -22,8 +22,8 @@ option(CHIAKI_GUI_ENABLE_SDL_GAMECONTROLLER "Use SDL Gamecontroller for Input" O option(CHIAKI_CLI_ARGP_STANDALONE "Search for standalone argp lib for CLI" OFF) set(CHIAKI_VERSION_MAJOR 1) -set(CHIAKI_VERSION_MINOR 2) -set(CHIAKI_VERSION_PATCH 1) +set(CHIAKI_VERSION_MINOR 3) +set(CHIAKI_VERSION_PATCH 0) set(CHIAKI_VERSION ${CHIAKI_VERSION_MAJOR}.${CHIAKI_VERSION_MINOR}.${CHIAKI_VERSION_PATCH}) set(CPACK_PACKAGE_NAME "chiaki") diff --git a/android/app/build.gradle b/android/app/build.gradle index 62e5984..22dc881 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -24,7 +24,7 @@ android { applicationId "com.metallic.chiaki" minSdkVersion 21 targetSdkVersion 29 - versionCode 6 + versionCode 7 versionName chiakiVersion externalNativeBuild { cmake { From e42110ce56fc257c21514e15f3bcdb1287d5d55b Mon Sep 17 00:00:00 2001 From: Tommy He Date: Mon, 19 Oct 2020 21:44:06 +0800 Subject: [PATCH 040/237] Update platform-build.md with additional libraries for Fedora (#336) --- doc/platform-build.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/platform-build.md b/doc/platform-build.md index 133e3c5..942cc6b 100644 --- a/doc/platform-build.md +++ b/doc/platform-build.md @@ -6,7 +6,7 @@ On Fedora, build dependencies can be installed via: ``` -sudo dnf install cmake make qt5-qtmultimedia-devel qt5-qtsvg-devel qt5-qtbase-gui ffmpeg-devel opus-devel openssl-devel python3-protobuf protobuf-c protobuf-devel qt5-rpm-macros SDL2-devel +sudo dnf install cmake make qt5-qtmultimedia-devel qt5-qtsvg-devel qt5-qtbase-gui ffmpeg-devel opus-devel openssl-devel python3-protobuf protobuf-c protobuf-devel qt5-rpm-macros SDL2-devel libevdev-devel systemd-devel ``` Then, Chiaki builds just like any other CMake project: From 1b05f071323e77540bc92e515d3dea38aead7890 Mon Sep 17 00:00:00 2001 From: Marcin Tolysz Date: Mon, 19 Oct 2020 14:44:28 +0100 Subject: [PATCH 041/237] Add CUDA Hardware Decoder (#335) --- gui/include/videodecoder.h | 2 ++ gui/src/settings.cpp | 3 ++- gui/src/settingsdialog.cpp | 3 ++- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/gui/include/videodecoder.h b/gui/include/videodecoder.h index 9fa1d5b..2285d06 100644 --- a/gui/include/videodecoder.h +++ b/gui/include/videodecoder.h @@ -39,6 +39,7 @@ typedef enum { HW_DECODE_VAAPI = 1, HW_DECODE_VDPAU = 2, HW_DECODE_VIDEOTOOLBOX = 3, + HW_DECODE_CUDA = 4, } HardwareDecodeEngine; @@ -47,6 +48,7 @@ static const QMap hardware_decode_engine_nam { HW_DECODE_VAAPI, "vaapi"}, { HW_DECODE_VDPAU, "vdpau"}, { HW_DECODE_VIDEOTOOLBOX, "videotoolbox"}, + { HW_DECODE_CUDA, "cuda"}, }; class VideoDecoderException: public Exception diff --git a/gui/src/settings.cpp b/gui/src/settings.cpp index 8b23f55..2f5bc95 100644 --- a/gui/src/settings.cpp +++ b/gui/src/settings.cpp @@ -98,7 +98,8 @@ static const QMap hw_decode_engine_values = { { HW_DECODE_NONE, "none" }, { HW_DECODE_VAAPI, "vaapi" }, { HW_DECODE_VDPAU, "vdpau" }, - { HW_DECODE_VIDEOTOOLBOX, "videotoolbox" } + { HW_DECODE_VIDEOTOOLBOX, "videotoolbox" }, + { HW_DECODE_CUDA, "cuda" } }; static const HardwareDecodeEngine hw_decode_engine_default = HW_DECODE_NONE; diff --git a/gui/src/settingsdialog.cpp b/gui/src/settingsdialog.cpp index 04352bc..56c63a6 100644 --- a/gui/src/settingsdialog.cpp +++ b/gui/src/settingsdialog.cpp @@ -159,7 +159,8 @@ SettingsDialog::SettingsDialog(Settings *settings, QWidget *parent) : QDialog(pa static const QList> hardware_decode_engines = { { HW_DECODE_NONE, "none"}, { HW_DECODE_VAAPI, "vaapi"}, - { HW_DECODE_VIDEOTOOLBOX, "videotoolbox"} + { HW_DECODE_VIDEOTOOLBOX, "videotoolbox"}, + { HW_DECODE_CUDA, "cuda"} }; auto current_hardware_decode_engine = settings->GetHardwareDecodeEngine(); for(const auto &p : hardware_decode_engines) From e5c0c805e2144ac4ba426e3e3c9ea7f27ad58875 Mon Sep 17 00:00:00 2001 From: AsciiWolf Date: Tue, 20 Oct 2020 20:29:20 +0200 Subject: [PATCH 042/237] Add AppStream metadata (#339) --- gui/CMakeLists.txt | 1 + gui/chiaki.appdata.xml | 26 ++++++++++++++++++++++++++ 2 files changed, 27 insertions(+) create mode 100644 gui/chiaki.appdata.xml diff --git a/gui/CMakeLists.txt b/gui/CMakeLists.txt index 7072b83..7eda103 100644 --- a/gui/CMakeLists.txt +++ b/gui/CMakeLists.txt @@ -103,3 +103,4 @@ install(TARGETS chiaki BUNDLE DESTINATION bin) install(FILES chiaki.desktop DESTINATION share/applications) install(FILES chiaki.png DESTINATION share/icons/hicolor/512x512/apps) +install(FILES chiaki.appdata.xml DESTINATION share/metainfo) diff --git a/gui/chiaki.appdata.xml b/gui/chiaki.appdata.xml new file mode 100644 index 0000000..d37826b --- /dev/null +++ b/gui/chiaki.appdata.xml @@ -0,0 +1,26 @@ + + + + com.metallic.chiaki + Chiaki + Free and Open Source Client for PlayStation 4 Remote Play + CC0-1.0 + GPL-3.0 + Florian Märkl + https://github.com/thestr4ng3r/chiaki + +

+ Chiaki is a Free and Open Source Client for PlayStation 4 Remote Play. It can be used to play in real time on a PlayStation 4 as long as there is a network connection. +

+

+ For more information and instructions how to use Chiaki, please refer to the official documentation: https://github.com/thestr4ng3r/chiaki/blob/master/README.md#usage +

+
+ + + https://raw.githubusercontent.com/thestr4ng3r/chiaki/master/assets/screenshot.png + + + chiaki@metallic.software + +
From 93cce70da552edb89a745ee43a2446e9d5e0121d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Tue, 20 Oct 2020 20:42:45 +0200 Subject: [PATCH 043/237] Fix Evdev and Udev for old CMake (#338) --- setsu/cmake/FindEvdev.cmake | 18 +++++++++++------- setsu/cmake/FindUdev.cmake | 17 +++++++++++------ 2 files changed, 22 insertions(+), 13 deletions(-) diff --git a/setsu/cmake/FindEvdev.cmake b/setsu/cmake/FindEvdev.cmake index 0703889..5ea7dea 100644 --- a/setsu/cmake/FindEvdev.cmake +++ b/setsu/cmake/FindEvdev.cmake @@ -4,13 +4,17 @@ set(_prefix Evdev) set(_target "${_prefix}::libevdev") find_package(PkgConfig) - -if(PkgConfig_FOUND) - pkg_check_modules("${_prefix}" libevdev) - if(${_prefix}_FOUND AND NOT TARGET "${_target}") - add_library("${_target}" INTERFACE IMPORTED) - target_link_libraries("${_target}" INTERFACE ${${_prefix}_LIBRARIES}) - target_include_directories("${_target}" INTERFACE ${${_prefix}_INCLUDE_DIRS}) +if(PkgConfig_FOUND AND NOT TARGET ${_target}) + pkg_check_modules("${_prefix}" libevdev IMPORTED_TARGET) + if((TARGET PkgConfig::${_prefix}) AND (NOT CMAKE_VERSION VERSION_LESS "3.11.0")) + set_target_properties(PkgConfig::${_prefix} PROPERTIES IMPORTED_GLOBAL ON) + add_library(${_target} ALIAS PkgConfig::${_prefix}) + elseif(${_prefix}_FOUND) + add_library(${_target} INTERFACE IMPORTED) + set_target_properties(${_target} PROPERTIES + INTERFACE_INCLUDE_DIRECTORIES "${${_prefix}_INCLUDE_DIRS}") + set_target_properties(${_target} PROPERTIES + INTERFACE_LINK_LIBRARIES "${${_prefix}_LIBRARIES}") endif() endif() diff --git a/setsu/cmake/FindUdev.cmake b/setsu/cmake/FindUdev.cmake index a35b5f6..fc1c81f 100644 --- a/setsu/cmake/FindUdev.cmake +++ b/setsu/cmake/FindUdev.cmake @@ -4,12 +4,17 @@ set(_prefix Udev) set(_target "${_prefix}::libudev") find_package(PkgConfig) -if(PkgConfig_FOUND) - pkg_check_modules("${_prefix}" libudev) - if(${_prefix}_FOUND AND NOT TARGET "${_target}") - add_library("${_target}" INTERFACE IMPORTED) - target_link_libraries("${_target}" INTERFACE ${${_prefix}_LIBRARIES}) - target_include_directories("${_target}" INTERFACE ${${_prefix}_INCLUDE_DIRS}) +if(PkgConfig_FOUND AND NOT TARGET ${_target}) + pkg_check_modules("${_prefix}" libudev IMPORTED_TARGET) + if((TARGET PkgConfig::${_prefix}) AND (NOT CMAKE_VERSION VERSION_LESS "3.11.0")) + set_target_properties(PkgConfig::${_prefix} PROPERTIES IMPORTED_GLOBAL ON) + add_library(${_target} ALIAS PkgConfig::${_prefix}) + elseif(${_prefix}_FOUND) + add_library(${_target} INTERFACE IMPORTED) + set_target_properties(${_target} PROPERTIES + INTERFACE_INCLUDE_DIRECTORIES "${${_prefix}_INCLUDE_DIRS}") + set_target_properties(${_target} PROPERTIES + INTERFACE_LINK_LIBRARIES "${${_prefix}_LIBRARIES}") endif() endif() From 732f312b1761b5a68ecc5596a459752bc82a1875 Mon Sep 17 00:00:00 2001 From: AsciiWolf Date: Tue, 20 Oct 2020 22:10:50 +0200 Subject: [PATCH 044/237] Fix AppStream metadata (#343) --- gui/CMakeLists.txt | 2 +- gui/{chiaki.appdata.xml => re.chiaki.Chiaki.appdata.xml} | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) rename gui/{chiaki.appdata.xml => re.chiaki.Chiaki.appdata.xml} (92%) diff --git a/gui/CMakeLists.txt b/gui/CMakeLists.txt index 7eda103..44c405b 100644 --- a/gui/CMakeLists.txt +++ b/gui/CMakeLists.txt @@ -103,4 +103,4 @@ install(TARGETS chiaki BUNDLE DESTINATION bin) install(FILES chiaki.desktop DESTINATION share/applications) install(FILES chiaki.png DESTINATION share/icons/hicolor/512x512/apps) -install(FILES chiaki.appdata.xml DESTINATION share/metainfo) +install(FILES re.chiaki.Chiaki.appdata.xml DESTINATION share/metainfo) diff --git a/gui/chiaki.appdata.xml b/gui/re.chiaki.Chiaki.appdata.xml similarity index 92% rename from gui/chiaki.appdata.xml rename to gui/re.chiaki.Chiaki.appdata.xml index d37826b..938a732 100644 --- a/gui/chiaki.appdata.xml +++ b/gui/re.chiaki.Chiaki.appdata.xml @@ -1,7 +1,8 @@ - com.metallic.chiaki + re.chiaki.Chiaki + chiaki.desktop Chiaki Free and Open Source Client for PlayStation 4 Remote Play CC0-1.0 From d5b220d7c30cc685f6016c6f21793c73688308d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Wed, 21 Oct 2020 11:59:36 +0200 Subject: [PATCH 045/237] Fix FindFFMPEG.cmake for older CMake (#344) --- cmake/FindFFMPEG.cmake | 36 ++++++++++++++++++++++-------------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/cmake/FindFFMPEG.cmake b/cmake/FindFFMPEG.cmake index 8ad5ec9..a1d0ecd 100644 --- a/cmake/FindFFMPEG.cmake +++ b/cmake/FindFFMPEG.cmake @@ -46,21 +46,29 @@ function (_ffmpeg_find component headername) if(PKG_CONFIG_FOUND) pkg_check_modules(FFMPEG_${component} lib${component} IMPORTED_TARGET) if(FFMPEG_${component}_FOUND) - if(APPLE) - join(FFMPEG_LDFLAGS_STRING " " ${FFMPEG_${component}_LDFLAGS}) - string(REGEX REPLACE "-Wl,-framework,([^ ]+)" "-framework \\1" FFMPEG_LDFLAGS_STRING_CLEAN ${FFMPEG_LDFLAGS_STRING}) - string(REGEX MATCHALL "-framework [^ ]+" FFMPEG_FRAMEWORKS ${FFMPEG_LDFLAGS_STRING_CLEAN}) - list(APPEND FFMPEG_${component}_LIBRARIES ${FFMPEG_FRAMEWORKS}) - set_target_properties(PkgConfig::FFMPEG_${component} PROPERTIES - INTERFACE_LINK_DIRECTORIES "${FFMPEG_${component}_LIBRARY_DIRS}" - INTERFACE_LINK_LIBRARIES "${FFMPEG_${component}_LIBRARIES}" - INTERFACE_LINK_OPTIONS "") - message("set libs to \"${FFMPEG_${component}_LIBRARIES}\"") - message("set lib dirs to \"${FFMPEG_${component}_LIBRARY_DIRS}\"") - message("set lib otps not to \"${FFMPEG_${component}_LDFLAGS}\"") + if((TARGET PkgConfig::FFMPEG_${component}) AND (NOT CMAKE_VERSION VERSION_LESS "3.11.0")) + if(APPLE) + join(FFMPEG_LDFLAGS_STRING " " ${FFMPEG_${component}_LDFLAGS}) + string(REGEX REPLACE "-Wl,-framework,([^ ]+)" "-framework \\1" FFMPEG_LDFLAGS_STRING_CLEAN ${FFMPEG_LDFLAGS_STRING}) + string(REGEX MATCHALL "-framework [^ ]+" FFMPEG_FRAMEWORKS ${FFMPEG_LDFLAGS_STRING_CLEAN}) + list(APPEND FFMPEG_${component}_LIBRARIES ${FFMPEG_FRAMEWORKS}) + set_target_properties(PkgConfig::FFMPEG_${component} PROPERTIES + INTERFACE_LINK_DIRECTORIES "${FFMPEG_${component}_LIBRARY_DIRS}" + INTERFACE_LINK_LIBRARIES "${FFMPEG_${component}_LIBRARIES}" + INTERFACE_LINK_OPTIONS "") + message("set libs to \"${FFMPEG_${component}_LIBRARIES}\"") + message("set lib dirs to \"${FFMPEG_${component}_LIBRARY_DIRS}\"") + message("set lib otps not to \"${FFMPEG_${component}_LDFLAGS}\"") + endif() + set_target_properties(PkgConfig::FFMPEG_${component} PROPERTIES IMPORTED_GLOBAL TRUE) + add_library(FFMPEG::${component} ALIAS PkgConfig::FFMPEG_${component}) + else() + add_library("FFMPEG::${component}" INTERFACE IMPORTED) + set_target_properties("FFMPEG::${component}" PROPERTIES + INTERFACE_LINK_DIRECTORIES "${FFMPEG_${component}_LIBRARY_DIRS}" + INTERFACE_INCLUDE_DIRECTORIES "${FFMPEG_${component}_INCLUDE_DIRS}" + INTERFACE_LINK_LIBRARIES "${FFMPEG_${component}_LIBRARIES}") endif() - set_target_properties(PkgConfig::FFMPEG_${component} PROPERTIES IMPORTED_GLOBAL TRUE) - add_library(FFMPEG::${component} ALIAS PkgConfig::FFMPEG_${component}) return() endif() endif() From a6e24ce325ff55296b24ab0a307093859c5ca786 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Wed, 21 Oct 2020 18:26:15 +0200 Subject: [PATCH 046/237] Fix Stack Free in CLI Stream --- gui/src/main.cpp | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/gui/src/main.cpp b/gui/src/main.cpp index fc011d5..f612b3d 100644 --- a/gui/src/main.cpp +++ b/gui/src/main.cpp @@ -169,11 +169,9 @@ int RunMain(QApplication &app, Settings *settings) return app.exec(); } - - int RunStream(QApplication &app, const StreamSessionConnectInfo &connect_info) { - StreamWindow window(connect_info); + StreamWindow *window = new StreamWindow(connect_info); app.setQuitOnLastWindowClosed(true); return app.exec(); } From 6052d9d7d7ed6ea497271e57da182646ebec5d3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Thu, 22 Oct 2020 12:39:58 +0200 Subject: [PATCH 047/237] Add Sleep Mode Trigger --- lib/include/chiaki/ctrl.h | 9 ++- lib/include/chiaki/session.h | 1 + lib/src/ctrl.c | 107 +++++++++++++++++++++++++++++------ lib/src/session.c | 21 +++++-- 4 files changed, 116 insertions(+), 22 deletions(-) diff --git a/lib/include/chiaki/ctrl.h b/lib/include/chiaki/ctrl.h index 71b0169..c07c0d2 100644 --- a/lib/include/chiaki/ctrl.h +++ b/lib/include/chiaki/ctrl.h @@ -33,6 +33,8 @@ extern "C" { #endif +typedef struct chiaki_ctrl_message_queue_t ChiakiCtrlMessageQueue; + typedef struct chiaki_ctrl_t { struct chiaki_session_t *session; @@ -42,6 +44,7 @@ typedef struct chiaki_ctrl_t bool login_pin_entered; uint8_t *login_pin; size_t login_pin_size; + ChiakiCtrlMessageQueue *msg_queue; ChiakiStopPipe notif_pipe; ChiakiMutex notif_mutex; @@ -59,10 +62,14 @@ typedef struct chiaki_ctrl_t uint64_t crypt_counter_remote; } ChiakiCtrl; -CHIAKI_EXPORT ChiakiErrorCode chiaki_ctrl_start(ChiakiCtrl *ctrl, struct chiaki_session_t *session); +CHIAKI_EXPORT ChiakiErrorCode chiaki_ctrl_init(ChiakiCtrl *ctrl, struct chiaki_session_t *session); +CHIAKI_EXPORT ChiakiErrorCode chiaki_ctrl_start(ChiakiCtrl *ctrl); CHIAKI_EXPORT void chiaki_ctrl_stop(ChiakiCtrl *ctrl); CHIAKI_EXPORT ChiakiErrorCode chiaki_ctrl_join(ChiakiCtrl *ctrl); +CHIAKI_EXPORT void chiaki_ctrl_fini(ChiakiCtrl *ctrl); +CHIAKI_EXPORT ChiakiErrorCode chiaki_ctrl_send_message(ChiakiCtrl *ctrl, uint16_t type, const uint8_t *payload, size_t payload_size); CHIAKI_EXPORT void chiaki_ctrl_set_login_pin(ChiakiCtrl *ctrl, const uint8_t *pin, size_t pin_size); +CHIAKI_EXPORT ChiakiErrorCode chiaki_ctrl_goto_bed(ChiakiCtrl *ctrl); #ifdef __cplusplus } diff --git a/lib/include/chiaki/session.h b/lib/include/chiaki/session.h index 81d5fe3..87d69d0 100644 --- a/lib/include/chiaki/session.h +++ b/lib/include/chiaki/session.h @@ -216,6 +216,7 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_session_stop(ChiakiSession *session); CHIAKI_EXPORT ChiakiErrorCode chiaki_session_join(ChiakiSession *session); CHIAKI_EXPORT ChiakiErrorCode chiaki_session_set_controller_state(ChiakiSession *session, ChiakiControllerState *state); CHIAKI_EXPORT ChiakiErrorCode chiaki_session_set_login_pin(ChiakiSession *session, const uint8_t *pin, size_t pin_size); +CHIAKI_EXPORT ChiakiErrorCode chiaki_session_goto_bed(ChiakiSession *session); static inline void chiaki_session_set_event_cb(ChiakiSession *session, ChiakiEventCallback cb, void *user) { diff --git a/lib/src/ctrl.c b/lib/src/ctrl.c index bca281f..44ede37 100644 --- a/lib/src/ctrl.c +++ b/lib/src/ctrl.c @@ -50,7 +50,8 @@ typedef enum ctrl_message_type_t { CTRL_MESSAGE_TYPE_HEARTBEAT_REP = 0x1fe, CTRL_MESSAGE_TYPE_LOGIN_PIN_REQ = 0x4, CTRL_MESSAGE_TYPE_LOGIN_PIN_REP = 0x8004, - CTRL_MESSAGE_TYPE_LOGIN = 0x5 + CTRL_MESSAGE_TYPE_LOGIN = 0x5, + CTRL_MESSAGE_TYPE_GOTO_BED = 0x50 } CtrlMessageType; typedef enum ctrl_login_state_t { @@ -58,15 +59,22 @@ typedef enum ctrl_login_state_t { CTRL_LOGIN_STATE_PIN_INCORRECT = 0x1 } CtrlLoginState; +struct chiaki_ctrl_message_queue_t +{ + ChiakiCtrlMessageQueue *next; + uint16_t type; + uint8_t *payload; + size_t payload_size; +}; static void *ctrl_thread_func(void *user); -static ChiakiErrorCode ctrl_message_send(ChiakiCtrl *ctrl, CtrlMessageType type, const uint8_t *payload, size_t payload_size); +static ChiakiErrorCode ctrl_message_send(ChiakiCtrl *ctrl, uint16_t type, const uint8_t *payload, size_t payload_size); static void ctrl_message_received_session_id(ChiakiCtrl *ctrl, uint8_t *payload, size_t payload_size); static void ctrl_message_received_heartbeat_req(ChiakiCtrl *ctrl, uint8_t *payload, size_t payload_size); static void ctrl_message_received_login_pin_req(ChiakiCtrl *ctrl, uint8_t *payload, size_t payload_size); static void ctrl_message_received_login(ChiakiCtrl *ctrl, uint8_t *payload, size_t payload_size); -CHIAKI_EXPORT ChiakiErrorCode chiaki_ctrl_start(ChiakiCtrl *ctrl, ChiakiSession *session) +CHIAKI_EXPORT ChiakiErrorCode chiaki_ctrl_init(ChiakiCtrl *ctrl, ChiakiSession *session) { ctrl->session = session; @@ -75,6 +83,7 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_ctrl_start(ChiakiCtrl *ctrl, ChiakiSession ctrl->login_pin_requested = false; ctrl->login_pin = NULL; ctrl->login_pin_size = 0; + ctrl->msg_queue = NULL; ChiakiErrorCode err = chiaki_stop_pipe_init(&ctrl->notif_pipe); if(err != CHIAKI_ERR_SUCCESS) @@ -84,13 +93,7 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_ctrl_start(ChiakiCtrl *ctrl, ChiakiSession if(err != CHIAKI_ERR_SUCCESS) goto error_notif_pipe; - err = chiaki_thread_create(&ctrl->thread, ctrl_thread_func, ctrl); - if(err != CHIAKI_ERR_SUCCESS) - goto error_notif_mutex; - - chiaki_thread_set_name(&ctrl->thread, "Chiaki Ctrl"); return err; - error_notif_mutex: chiaki_mutex_fini(&ctrl->notif_mutex); error_notif_pipe: @@ -98,6 +101,16 @@ error_notif_pipe: return err; } +CHIAKI_EXPORT ChiakiErrorCode chiaki_ctrl_start(ChiakiCtrl *ctrl) +{ + ChiakiErrorCode err = chiaki_thread_create(&ctrl->thread, ctrl_thread_func, ctrl); + if(err != CHIAKI_ERR_SUCCESS) + return err; + + chiaki_thread_set_name(&ctrl->thread, "Chiaki Ctrl"); + return err; +} + CHIAKI_EXPORT void chiaki_ctrl_stop(ChiakiCtrl *ctrl) { ChiakiErrorCode err = chiaki_mutex_lock(&ctrl->notif_mutex); @@ -109,11 +122,57 @@ CHIAKI_EXPORT void chiaki_ctrl_stop(ChiakiCtrl *ctrl) CHIAKI_EXPORT ChiakiErrorCode chiaki_ctrl_join(ChiakiCtrl *ctrl) { - ChiakiErrorCode err = chiaki_thread_join(&ctrl->thread, NULL); + return chiaki_thread_join(&ctrl->thread, NULL); +} + +CHIAKI_EXPORT void chiaki_ctrl_fini(ChiakiCtrl *ctrl) +{ chiaki_stop_pipe_fini(&ctrl->notif_pipe); chiaki_mutex_fini(&ctrl->notif_mutex); free(ctrl->login_pin); - return err; +} + +static void ctrl_message_queue_free(ChiakiCtrlMessageQueue *queue) +{ + free(queue->payload); + free(queue); +} + +CHIAKI_EXPORT ChiakiErrorCode chiaki_ctrl_send_message(ChiakiCtrl *ctrl, uint16_t type, const uint8_t *payload, size_t payload_size) +{ + ChiakiCtrlMessageQueue *queue = CHIAKI_NEW(ChiakiCtrlMessageQueue); + if(!queue) + return CHIAKI_ERR_MEMORY; + queue->next = NULL; + queue->type = type; + if(payload) + { + queue->payload = malloc(payload_size); + if(!queue->payload) + { + free(queue); + return CHIAKI_ERR_MEMORY; + } + queue->payload_size = payload_size; + } + else + { + queue->payload = NULL; + queue->payload_size = 0; + } + ChiakiErrorCode err = chiaki_mutex_lock(&ctrl->notif_mutex); + assert(err == CHIAKI_ERR_SUCCESS); + if(!ctrl->msg_queue) + ctrl->msg_queue = queue; + else + { + ChiakiCtrlMessageQueue *c = ctrl->msg_queue; + while(c->next) + c = c->next; + c->next = queue; + } + chiaki_mutex_unlock(&ctrl->notif_mutex); + chiaki_stop_pipe_stop(&ctrl->notif_pipe); } CHIAKI_EXPORT void chiaki_ctrl_set_login_pin(ChiakiCtrl *ctrl, const uint8_t *pin, size_t pin_size) @@ -133,6 +192,10 @@ CHIAKI_EXPORT void chiaki_ctrl_set_login_pin(ChiakiCtrl *ctrl, const uint8_t *pi chiaki_mutex_unlock(&ctrl->notif_mutex); } +CHIAKI_EXPORT ChiakiErrorCode chiaki_ctrl_goto_bed(ChiakiCtrl *ctrl) +{ + return chiaki_ctrl_send_message(ctrl, CTRL_MESSAGE_TYPE_GOTO_BED, NULL, 0); +} static ChiakiErrorCode ctrl_connect(ChiakiCtrl *ctrl); static void ctrl_message_received(ChiakiCtrl *ctrl, uint16_t msg_type, uint8_t *payload, size_t payload_size); @@ -202,10 +265,12 @@ static void *ctrl_thread_func(void *user) chiaki_mutex_lock(&ctrl->notif_mutex); if(err == CHIAKI_ERR_CANCELED) { - if(ctrl->should_stop) + while(ctrl->msg_queue) { - CHIAKI_LOGI(ctrl->session->log, "Ctrl requested to stop"); - break; + ctrl_message_send(ctrl, ctrl->msg_queue->type, ctrl->msg_queue->payload, ctrl->msg_queue->payload_size); + ChiakiCtrlMessageQueue *next = ctrl->msg_queue->next; + ctrl_message_queue_free(ctrl->msg_queue); + ctrl->msg_queue = next; } if(ctrl->login_pin_entered) @@ -219,11 +284,14 @@ static void *ctrl_thread_func(void *user) chiaki_stop_pipe_reset(&ctrl->notif_pipe); continue; } - else + + if(ctrl->should_stop) { - CHIAKI_LOGE(ctrl->session->log, "Ctrl notif pipe set without state"); + CHIAKI_LOGI(ctrl->session->log, "Ctrl requested to stop"); break; } + + continue; } else if(err != CHIAKI_ERR_SUCCESS) { @@ -252,10 +320,15 @@ static void *ctrl_thread_func(void *user) return NULL; } -static ChiakiErrorCode ctrl_message_send(ChiakiCtrl *ctrl, CtrlMessageType type, const uint8_t *payload, size_t payload_size) +static ChiakiErrorCode ctrl_message_send(ChiakiCtrl *ctrl, uint16_t type, const uint8_t *payload, size_t payload_size) { assert(payload_size == 0 || payload); + CHIAKI_LOGV(ctrl->session->log, "Ctrl sending message type %x, size %llx\n", + (unsigned int)type, (unsigned long long)payload_size); + if(payload) + chiaki_log_hexdump(ctrl->session->log, CHIAKI_LOG_VERBOSE, payload, payload_size); + uint8_t *enc = NULL; if(payload && payload_size) { diff --git a/lib/src/session.c b/lib/src/session.c index caf8236..4b1cc2a 100644 --- a/lib/src/session.c +++ b/lib/src/session.c @@ -170,8 +170,6 @@ CHIAKI_EXPORT const char *chiaki_quit_reason_string(ChiakiQuitReason reason) } } - - CHIAKI_EXPORT ChiakiErrorCode chiaki_session_init(ChiakiSession *session, ChiakiConnectInfo *connect_info, ChiakiLog *log) { memset(session, 0, sizeof(ChiakiSession)); @@ -199,11 +197,18 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_session_init(ChiakiSession *session, Chiaki session->login_pin = NULL; session->login_pin_size = 0; + err = chiaki_ctrl_init(&session->ctrl, session); + if(err != CHIAKI_ERR_SUCCESS) + { + CHIAKI_LOGE(session->log, "Ctrl init failed"); + goto error_stop_pipe; + } + err = chiaki_stream_connection_init(&session->stream_connection, session); if(err != CHIAKI_ERR_SUCCESS) { CHIAKI_LOGE(session->log, "StreamConnection init failed"); - goto error_stop_pipe; + goto error_ctrl; } int r = getaddrinfo(connect_info->host, NULL, NULL, &session->connect_info.host_addrinfos); @@ -229,6 +234,8 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_session_init(ChiakiSession *session, Chiaki return CHIAKI_ERR_SUCCESS; error_stop_pipe: chiaki_stop_pipe_fini(&session->stop_pipe); +error_ctrl: + chiaki_ctrl_fini(&session->ctrl); error_state_mutex: chiaki_mutex_fini(&session->state_mutex); error_state_cond: @@ -244,6 +251,7 @@ CHIAKI_EXPORT void chiaki_session_fini(ChiakiSession *session) free(session->login_pin); free(session->quit_reason_str); chiaki_stream_connection_fini(&session->stream_connection); + chiaki_ctrl_fini(&session->ctrl); chiaki_stop_pipe_fini(&session->stop_pipe); chiaki_cond_fini(&session->state_cond); chiaki_mutex_fini(&session->state_mutex); @@ -402,7 +410,7 @@ static void *session_thread_func(void *arg) CHIAKI_LOGI(session->log, "Starting ctrl"); - ChiakiErrorCode err = chiaki_ctrl_start(&session->ctrl, session); + ChiakiErrorCode err = chiaki_ctrl_start(&session->ctrl); if(err != CHIAKI_ERR_SUCCESS) QUIT(quit); @@ -832,3 +840,8 @@ static bool session_thread_request_session(ChiakiSession *session, ChiakiTarget CHIAKI_SOCKET_CLOSE(session_sock); return response.success; } + +CHIAKI_EXPORT ChiakiErrorCode chiaki_session_goto_bed(ChiakiSession *session) +{ + return chiaki_ctrl_goto_bed(&session->ctrl); +} From 8ddbad6f619cb1b296cd2cda54fc0b6b044b0d3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Thu, 22 Oct 2020 13:53:31 +0200 Subject: [PATCH 048/237] Add Sleep Trigger to GUI --- gui/include/settings.h | 10 +++++++++ gui/include/settingsdialog.h | 2 ++ gui/include/streamsession.h | 5 +++++ gui/include/streamwindow.h | 3 ++- gui/src/settings.cpp | 19 ++++++++++++++++ gui/src/settingsdialog.cpp | 24 ++++++++++++++++++++- gui/src/streamsession.cpp | 9 ++++++++ gui/src/streamwindow.cpp | 42 ++++++++++++++++++++++++++++++++---- 8 files changed, 108 insertions(+), 6 deletions(-) diff --git a/gui/include/settings.h b/gui/include/settings.h index c52b42f..1088185 100644 --- a/gui/include/settings.h +++ b/gui/include/settings.h @@ -38,6 +38,13 @@ enum class ControllerButtonExt ANALOG_STICK_RIGHT_Y_DOWN = (1 << 25), }; +enum class DisconnectAction +{ + AlwaysNothing, + AlwaysSleep, + Ask +}; + class Settings : public QObject { Q_OBJECT @@ -95,6 +102,9 @@ class Settings : public QObject ChiakiConnectVideoProfile GetVideoProfile(); + DisconnectAction GetDisconnectAction(); + void SetDisconnectAction(DisconnectAction action); + QList GetRegisteredHosts() const { return registered_hosts.values(); } void AddRegisteredHost(const RegisteredHost &host); void RemoveRegisteredHost(const HostMAC &mac); diff --git a/gui/include/settingsdialog.h b/gui/include/settingsdialog.h index 13a40c9..90235c9 100644 --- a/gui/include/settingsdialog.h +++ b/gui/include/settingsdialog.h @@ -34,6 +34,7 @@ class SettingsDialog : public QDialog Settings *settings; QCheckBox *log_verbose_check_box; + QComboBox *disconnect_action_combo_box; QComboBox *resolution_combo_box; QComboBox *fps_combo_box; @@ -48,6 +49,7 @@ class SettingsDialog : public QDialog private slots: void LogVerboseChanged(); + void DisconnectActionSelected(); void ResolutionSelected(); void FPSSelected(); diff --git a/gui/include/streamsession.h b/gui/include/streamsession.h index 94399ad..0790b48 100644 --- a/gui/include/streamsession.h +++ b/gui/include/streamsession.h @@ -48,6 +48,7 @@ class ChiakiException: public Exception struct StreamSessionConnectInfo { + Settings *settings; QMap key_map; HardwareDecodeEngine hw_decode_engine; uint32_t log_level_mask; @@ -71,6 +72,7 @@ class StreamSession : public QObject SessionLog log; ChiakiSession session; ChiakiOpusDecoder opus_decoder; + bool connected; Controller *controller; #if CHIAKI_GUI_ENABLE_SETSU @@ -103,8 +105,11 @@ class StreamSession : public QObject explicit StreamSession(const StreamSessionConnectInfo &connect_info, QObject *parent = nullptr); ~StreamSession(); + bool IsConnected() { return connected; } + void Start(); void Stop(); + void GoToBed(); void SetLoginPIN(const QString &pin); diff --git a/gui/include/streamwindow.h b/gui/include/streamwindow.h index 3e664bc..b554cc7 100644 --- a/gui/include/streamwindow.h +++ b/gui/include/streamwindow.h @@ -34,11 +34,12 @@ class StreamWindow: public QMainWindow ~StreamWindow() override; private: + const StreamSessionConnectInfo connect_info; StreamSession *session; AVOpenGLWidget *av_widget; - void Init(const StreamSessionConnectInfo &connect_info); + void Init(); protected: void keyPressEvent(QKeyEvent *event) override; diff --git a/gui/src/settings.cpp b/gui/src/settings.cpp index 2f5bc95..d8653aa 100644 --- a/gui/src/settings.cpp +++ b/gui/src/settings.cpp @@ -136,6 +136,25 @@ ChiakiConnectVideoProfile Settings::GetVideoProfile() return profile; } +static const QMap disconnect_action_values = { + { DisconnectAction::Ask, "ask" }, + { DisconnectAction::AlwaysNothing, "nothing" }, + { DisconnectAction::AlwaysSleep, "sleep" } +}; + +static const DisconnectAction disconnect_action_default = DisconnectAction::Ask; + +DisconnectAction Settings::GetDisconnectAction() +{ + auto v = settings.value("settings/disconnect_action", disconnect_action_values[disconnect_action_default]).toString(); + return disconnect_action_values.key(v, disconnect_action_default); +} + +void Settings::SetDisconnectAction(DisconnectAction action) +{ + settings.setValue("settings/disconnect_action", disconnect_action_values[action]); +} + void Settings::LoadRegisteredHosts() { registered_hosts.clear(); diff --git a/gui/src/settingsdialog.cpp b/gui/src/settingsdialog.cpp index 56c63a6..146629e 100644 --- a/gui/src/settingsdialog.cpp +++ b/gui/src/settingsdialog.cpp @@ -85,6 +85,23 @@ SettingsDialog::SettingsDialog(Settings *settings, QWidget *parent) : QDialog(pa log_directory_label->setReadOnly(true); general_layout->addRow(tr("Log Directory:"), log_directory_label); + disconnect_action_combo_box = new QComboBox(this); + QList> disconnect_action_strings = { + { DisconnectAction::AlwaysNothing, "Do Nothing"}, + { DisconnectAction::AlwaysSleep, "Enter Sleep Mode"}, + { DisconnectAction::Ask, "Ask"} + }; + auto current_disconnect_action = settings->GetDisconnectAction(); + for(const auto &p : disconnect_action_strings) + { + disconnect_action_combo_box->addItem(tr(p.second), (int)p.first); + if(current_disconnect_action == p.first) + disconnect_action_combo_box->setCurrentIndex(disconnect_action_combo_box->count() - 1); + } + connect(disconnect_action_combo_box, SIGNAL(currentIndexChanged(int)), this, SLOT(DisconnectActionSelected())); + + general_layout->addRow(tr("Action on Disconnect:"), disconnect_action_combo_box); + auto about_button = new QPushButton(tr("About Chiaki"), this); general_layout->addRow(about_button); connect(about_button, &QPushButton::clicked, this, [this]() { @@ -140,7 +157,7 @@ SettingsDialog::SettingsDialog(Settings *settings, QWidget *parent) : QDialog(pa UpdateBitratePlaceholder(); audio_buffer_size_edit = new QLineEdit(this); - audio_buffer_size_edit->setValidator(new QIntValidator(1024, 0x20000)); + audio_buffer_size_edit->setValidator(new QIntValidator(1024, 0x20000, audio_buffer_size_edit)); unsigned int audio_buffer_size = settings->GetAudioBufferSizeRaw(); audio_buffer_size_edit->setText(audio_buffer_size ? QString::number(audio_buffer_size) : ""); stream_settings_layout->addRow(tr("Audio Buffer Size:"), audio_buffer_size_edit); @@ -249,6 +266,11 @@ void SettingsDialog::ResolutionSelected() UpdateBitratePlaceholder(); } +void SettingsDialog::DisconnectActionSelected() +{ + settings->SetDisconnectAction(static_cast(disconnect_action_combo_box->currentData().toInt())); +} + void SettingsDialog::LogVerboseChanged() { settings->SetLogVerbose(log_verbose_check_box->isChecked()); diff --git a/gui/src/streamsession.cpp b/gui/src/streamsession.cpp index dd4ddfd..ef3a62d 100644 --- a/gui/src/streamsession.cpp +++ b/gui/src/streamsession.cpp @@ -30,6 +30,7 @@ #define SETSU_UPDATE_INTERVAL_MS 4 StreamSessionConnectInfo::StreamSessionConnectInfo(Settings *settings, QString host, QByteArray regist_key, QByteArray morning) + : settings(settings) { key_map = settings->GetControllerMappingForDecoding(); hw_decode_engine = settings->GetHardwareDecodeEngine(); @@ -58,6 +59,7 @@ StreamSession::StreamSession(const StreamSessionConnectInfo &connect_info, QObje audio_output(nullptr), audio_io(nullptr) { + connected = false; chiaki_opus_decoder_init(&opus_decoder, log.GetChiakiLog()); audio_buffer_size = connect_info.audio_buffer_size; @@ -135,6 +137,11 @@ void StreamSession::Stop() chiaki_session_stop(&session); } +void StreamSession::GoToBed() +{ + chiaki_session_goto_bed(&session); +} + void StreamSession::SetLoginPIN(const QString &pin) { QByteArray data = pin.toUtf8(); @@ -299,8 +306,10 @@ void StreamSession::Event(ChiakiEvent *event) switch(event->type) { case CHIAKI_EVENT_CONNECTED: + connected = true; break; case CHIAKI_EVENT_QUIT: + connected = false; emit SessionQuit(event->quit.reason, event->quit.reason_str ? QString::fromUtf8(event->quit.reason_str) : QString()); break; case CHIAKI_EVENT_LOGIN_PIN_REQUEST: diff --git a/gui/src/streamwindow.cpp b/gui/src/streamwindow.cpp index 91ae8b0..678d40c 100644 --- a/gui/src/streamwindow.cpp +++ b/gui/src/streamwindow.cpp @@ -19,6 +19,7 @@ #include #include #include +#include #include #include @@ -26,7 +27,8 @@ #include StreamWindow::StreamWindow(const StreamSessionConnectInfo &connect_info, QWidget *parent) - : QMainWindow(parent) + : QMainWindow(parent), + connect_info(connect_info) { setAttribute(Qt::WA_DeleteOnClose); setWindowTitle(qApp->applicationName() + " | Stream"); @@ -36,7 +38,7 @@ StreamWindow::StreamWindow(const StreamSessionConnectInfo &connect_info, QWidget try { - Init(connect_info); + Init(); } catch(const Exception &e) { @@ -51,7 +53,7 @@ StreamWindow::~StreamWindow() delete av_widget; } -void StreamWindow::Init(const StreamSessionConnectInfo &connect_info) +void StreamWindow::Init() { session = new StreamSession(connect_info, this); @@ -98,10 +100,42 @@ void StreamWindow::mouseReleaseEvent(QMouseEvent *event) session->HandleMouseEvent(event); } -void StreamWindow::closeEvent(QCloseEvent *) +void StreamWindow::closeEvent(QCloseEvent *event) { if(session) + { + if(session->IsConnected()) + { + bool sleep = false; + switch(connect_info.settings->GetDisconnectAction()) + { + case DisconnectAction::Ask: { + auto res = QMessageBox::question(this, tr("Disconnect Session"), tr("Do you want the PS4 to go into sleep mode?"), + QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel); + switch(res) + { + case QMessageBox::Yes: + sleep = true; + break; + case QMessageBox::Cancel: + event->ignore(); + return; + default: + break; + } + break; + } + case DisconnectAction::AlwaysSleep: + sleep = true; + break; + default: + break; + } + if(sleep) + session->GoToBed(); + } session->Stop(); + } } void StreamWindow::SessionQuit(ChiakiQuitReason reason, const QString &reason_str) From 9200c0b893033cd13b501a1bf0d32de7f4a97c7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Thu, 22 Oct 2020 23:14:54 +0200 Subject: [PATCH 049/237] Align FEC Units by 16 --- lib/include/chiaki/fec.h | 2 +- lib/include/chiaki/frameprocessor.h | 1 + lib/src/fec.c | 8 +++++--- lib/src/frameprocessor.c | 16 ++++++++++------ test/fec.c | 24 ++++++++++++++---------- 5 files changed, 31 insertions(+), 20 deletions(-) diff --git a/lib/include/chiaki/fec.h b/lib/include/chiaki/fec.h index 105d3e7..658c7c0 100644 --- a/lib/include/chiaki/fec.h +++ b/lib/include/chiaki/fec.h @@ -31,7 +31,7 @@ extern "C" { #define CHIAKI_FEC_WORDSIZE 8 -CHIAKI_EXPORT ChiakiErrorCode chiaki_fec_decode(uint8_t *frame_buf, size_t unit_size, unsigned int k, unsigned int m, const unsigned int *erasures, size_t erasures_count); +CHIAKI_EXPORT ChiakiErrorCode chiaki_fec_decode(uint8_t *frame_buf, size_t unit_size, size_t stride, unsigned int k, unsigned int m, const unsigned int *erasures, size_t erasures_count); #ifdef __cplusplus } diff --git a/lib/include/chiaki/frameprocessor.h b/lib/include/chiaki/frameprocessor.h index aae7172..f296e02 100644 --- a/lib/include/chiaki/frameprocessor.h +++ b/lib/include/chiaki/frameprocessor.h @@ -37,6 +37,7 @@ typedef struct chiaki_frame_processor_t uint8_t *frame_buf; size_t frame_buf_size; size_t buf_size_per_unit; + size_t buf_stride_per_unit; unsigned int units_source_expected; unsigned int units_fec_expected; unsigned int units_source_received; diff --git a/lib/src/fec.c b/lib/src/fec.c index ea72c76..c797e68 100644 --- a/lib/src/fec.c +++ b/lib/src/fec.c @@ -28,8 +28,10 @@ int *create_matrix(unsigned int k, unsigned int m) return cauchy_original_coding_matrix(k, m, CHIAKI_FEC_WORDSIZE); } -CHIAKI_EXPORT ChiakiErrorCode chiaki_fec_decode(uint8_t *frame_buf, size_t unit_size, unsigned int k, unsigned int m, const unsigned int *erasures, size_t erasures_count) +CHIAKI_EXPORT ChiakiErrorCode chiaki_fec_decode(uint8_t *frame_buf, size_t unit_size, size_t stride, unsigned int k, unsigned int m, const unsigned int *erasures, size_t erasures_count) { + if(stride < unit_size) + return CHIAKI_ERR_INVALID_DATA; int *matrix = create_matrix(k, m); if(!matrix) return CHIAKI_ERR_MEMORY; @@ -61,7 +63,7 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_fec_decode(uint8_t *frame_buf, size_t unit_ for(size_t i=0; ilog = log; frame_processor->frame_buf = NULL; frame_processor->frame_buf_size = 0; + frame_processor->buf_size_per_unit = 0; + frame_processor->buf_stride_per_unit = 0; frame_processor->units_source_expected = 0; frame_processor->units_fec_expected = 0; frame_processor->unit_slots = NULL; @@ -77,6 +79,7 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_frame_processor_alloc_frame(ChiakiFrameProc } frame_processor->buf_size_per_unit += ntohs(((chiaki_unaligned_uint16_t *)packet->data)[0]); } + frame_processor->buf_stride_per_unit = ((frame_processor->buf_size_per_unit + 0xf) / 0x10) * 0x10; if(frame_processor->buf_size_per_unit == 0) { @@ -116,9 +119,9 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_frame_processor_alloc_frame(ChiakiFrameProc } memset(frame_processor->unit_slots, 0, frame_processor->unit_slots_size * sizeof(ChiakiFrameUnit)); - if(frame_processor->unit_slots_size > SIZE_MAX / frame_processor->buf_size_per_unit) + if(frame_processor->unit_slots_size > SIZE_MAX / frame_processor->buf_stride_per_unit) return CHIAKI_ERR_OVERFLOW; - size_t frame_buf_size_required = frame_processor->unit_slots_size * frame_processor->buf_size_per_unit; + size_t frame_buf_size_required = frame_processor->unit_slots_size * frame_processor->buf_stride_per_unit; if(frame_processor->frame_buf_size < frame_buf_size_required) { free(frame_processor->frame_buf); @@ -163,7 +166,7 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_frame_processor_put_unit(ChiakiFrameProcess } unit->data_size = packet->data_size; - memcpy(frame_processor->frame_buf + packet->unit_index * frame_processor->buf_size_per_unit, + memcpy(frame_processor->frame_buf + packet->unit_index * frame_processor->buf_stride_per_unit, packet->data, packet->data_size); @@ -206,7 +209,8 @@ static ChiakiErrorCode chiaki_frame_processor_fec(ChiakiFrameProcessor *frame_pr } assert(erasure_index == erasures_count); - ChiakiErrorCode err = chiaki_fec_decode(frame_processor->frame_buf, frame_processor->buf_size_per_unit, + ChiakiErrorCode err = chiaki_fec_decode(frame_processor->frame_buf, + frame_processor->buf_size_per_unit, frame_processor->buf_stride_per_unit, frame_processor->units_source_expected, frame_processor->units_fec_received, erasures, erasures_count); @@ -224,7 +228,7 @@ static ChiakiErrorCode chiaki_frame_processor_fec(ChiakiFrameProcessor *frame_pr for(size_t i=0; iunits_source_expected; i++) { ChiakiFrameUnit *slot = frame_processor->unit_slots + i; - uint8_t *buf_ptr = frame_processor->frame_buf + frame_processor->buf_size_per_unit * i; + uint8_t *buf_ptr = frame_processor->frame_buf + frame_processor->buf_stride_per_unit * i; uint16_t padding = ntohs(*((chiaki_unaligned_uint16_t *)buf_ptr)); if(padding >= frame_processor->buf_size_per_unit) { @@ -272,7 +276,7 @@ CHIAKI_EXPORT ChiakiFrameProcessorFlushResult chiaki_frame_processor_flush(Chiak continue; } size_t part_size = unit->data_size - 2; - uint8_t *buf_ptr = frame_processor->frame_buf + i*frame_processor->buf_size_per_unit; + uint8_t *buf_ptr = frame_processor->frame_buf + i*frame_processor->buf_stride_per_unit; memmove(frame_processor->frame_buf + cur, buf_ptr + 2, part_size); cur += part_size; } diff --git a/test/fec.c b/test/fec.c index 44848fe..d58107b 100644 --- a/test/fec.c +++ b/test/fec.c @@ -34,17 +34,20 @@ typedef struct fec_test_case_t static MunitResult test_fec_case(FECTestCase *test_case) { size_t b64len = strlen(test_case->frame_buffer_b64); - size_t frame_buffer_size = b64len; - - uint8_t *frame_buffer_ref = malloc(frame_buffer_size); + uint8_t *frame_buffer_ref = malloc(b64len); munit_assert_not_null(frame_buffer_ref); + + size_t stride = ((test_case->unit_size + 0xf) / 0x10) * 0x10; + size_t frame_buffer_size = stride * (test_case->k + test_case->m); uint8_t *frame_buffer = malloc(frame_buffer_size); munit_assert_not_null(frame_buffer); - ChiakiErrorCode err = chiaki_base64_decode(test_case->frame_buffer_b64, b64len, frame_buffer_ref, &frame_buffer_size); + ChiakiErrorCode err = chiaki_base64_decode(test_case->frame_buffer_b64, b64len, frame_buffer_ref, &b64len); munit_assert_int(err, ==, CHIAKI_ERR_SUCCESS); - munit_assert_size(frame_buffer_size, ==, test_case->unit_size * (test_case->k + test_case->m)); - memcpy(frame_buffer, frame_buffer_ref, frame_buffer_size); + munit_assert_size(b64len, ==, test_case->unit_size * (test_case->k + test_case->m)); + + for(size_t i=0; ik + test_case->m; i++) + memcpy(frame_buffer + i * stride, frame_buffer_ref + i * test_case->unit_size, test_case->unit_size); size_t erasures_count = 0; for(const int *e = test_case->erasures; *e >= 0; e++, erasures_count++); @@ -54,13 +57,14 @@ static MunitResult test_fec_case(FECTestCase *test_case) { unsigned int e = test_case->erasures[i]; munit_assert_uint(e, <, test_case->k + test_case->m); - memset(frame_buffer + test_case->unit_size * e, 0x42, test_case->unit_size); + memset(frame_buffer + stride * e, 0x42, test_case->unit_size); } - err = chiaki_fec_decode(frame_buffer, test_case->unit_size, test_case->k, test_case->m, (const unsigned int *)test_case->erasures, erasures_count); + err = chiaki_fec_decode(frame_buffer, test_case->unit_size, stride, test_case->k, test_case->m, (const unsigned int *)test_case->erasures, erasures_count); munit_assert_int(err, ==, CHIAKI_ERR_SUCCESS); - munit_assert_memory_equal(test_case->k * test_case->unit_size, frame_buffer, frame_buffer_ref); + for(size_t i=0; ik; i++) + munit_assert_memory_equal(test_case->unit_size, frame_buffer + i * stride, frame_buffer_ref + i * test_case->unit_size); free(frame_buffer); free(frame_buffer_ref); @@ -88,4 +92,4 @@ MunitTest tests_fec[] = { fec_params }, { NULL, NULL, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL } -}; \ No newline at end of file +}; From 03c82ea51540ba373437e0b68fe1c906540d42f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Thu, 22 Oct 2020 22:25:50 +0200 Subject: [PATCH 050/237] Optionally use system jerasure and add Bullseye CI --- .github/workflows/bullseye.yml | 22 ++++++++++++ CMakeLists.txt | 11 ++++++ cmake/FindJerasure.cmake | 26 ++++++++++++++ lib/CMakeLists.txt | 2 +- scripts/Dockerfile.bullseye | 12 +++++++ scripts/ci-script | 9 +++++ third-party/CMakeLists.txt | 64 ++++++++++++++++++---------------- 7 files changed, 115 insertions(+), 31 deletions(-) create mode 100644 .github/workflows/bullseye.yml create mode 100644 cmake/FindJerasure.cmake create mode 100644 scripts/Dockerfile.bullseye create mode 100755 scripts/ci-script diff --git a/.github/workflows/bullseye.yml b/.github/workflows/bullseye.yml new file mode 100644 index 0000000..cc736a3 --- /dev/null +++ b/.github/workflows/bullseye.yml @@ -0,0 +1,22 @@ +name: Debian Bullseye +on: + push: + branches: + - master + pull_request: + +jobs: + build: + name: Debian Bullseye + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Checkout submodules + run: | + git submodule init + git submodule update + - name: Docker Build + run: cd scripts && docker build -t chiaki-bullseye . -f Dockerfile.bullseye && cd .. + - name: Build and Test + run: docker run --rm -v "`pwd`:/build" -t chiaki-bullseye /bin/bash -c "cd /build && scripts/ci-script -DCHIAKI_USE_SYSTEM_JERASURE=ON" + diff --git a/CMakeLists.txt b/CMakeLists.txt index d59f031..f75bb09 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -20,6 +20,7 @@ option(CHIAKI_LIB_ENABLE_MBEDTLS "Use mbedtls instead of OpenSSL as part of Chia option(CHIAKI_LIB_OPENSSL_EXTERNAL_PROJECT "Use OpenSSL as CMake external project" OFF) option(CHIAKI_GUI_ENABLE_SDL_GAMECONTROLLER "Use SDL Gamecontroller for Input" ON) option(CHIAKI_CLI_ARGP_STANDALONE "Search for standalone argp lib for CLI" OFF) +tri_option(CHIAKI_USE_SYSTEM_JERASURE "Use system-provided jerasure instead of submodule" AUTO) set(CHIAKI_VERSION_MAJOR 1) set(CHIAKI_VERSION_MINOR 3) @@ -52,6 +53,16 @@ elseif(CHIAKI_ENABLE_SWITCH) set(CHIAKI_LIB_ENABLE_MBEDTLS ON) endif() +if(CHIAKI_USE_SYSTEM_JERASURE) + if(CHIAKI_USE_SYSTEM_JERASURE STREQUAL AUTO) + find_package(Jerasure QUIET) + set(CHIAKI_USE_SYSTEM_JERASURE ${Jerasure_FOUND}) + else() + find_package(Jerasure REQUIRED) + set(CHIAKI_USE_SYSTEM_JERASURE ON) + endif() +endif() + add_subdirectory(third-party) add_definitions(-DCHIAKI_VERSION_MAJOR=${CHIAKI_VERSION_MAJOR} -DCHIAKI_VERSION_MINOR=${CHIAKI_VERSION_MINOR} -DCHIAKI_VERSION_PATCH=${CHIAKI_VERSION_PATCH} -DCHIAKI_VERSION=\"${CHIAKI_VERSION}\") diff --git a/cmake/FindJerasure.cmake b/cmake/FindJerasure.cmake new file mode 100644 index 0000000..973b5a6 --- /dev/null +++ b/cmake/FindJerasure.cmake @@ -0,0 +1,26 @@ +# Provides Jerasure::Jerasure + +find_path(Jerasure_INCLUDE_DIR NAMES jerasure.h) +find_path(Jerasure_INCLUDE_DIR2 NAMES galois.h PATH_SUFFIXES jerasure) +find_library(Jerasure_LIBRARY NAMES Jerasure) + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(Jerasure + FOUND_VAR Jerasure_FOUND + REQUIRED_VARS + Jerasure_LIBRARY + Jerasure_INCLUDE_DIR + Jerasure_INCLUDE_DIR2 +) + +if(Jerasure_FOUND) + set(Jerasure_LIBRARIES ${Jerasure_LIBRARY}) + set(Jerasure_INCLUDE_DIRS ${Jerasure_INCLUDE_DIR} ${Jerasure_INCLUDE_DIR2}) + if(NOT TARGET Jerasure::Jerasure) + add_library(Jerasure::Jerasure UNKNOWN IMPORTED) + set_target_properties(Jerasure::Jerasure PROPERTIES + IMPORTED_LOCATION "${Jerasure_LIBRARY}" + INTERFACE_INCLUDE_DIRECTORIES "${Jerasure_INCLUDE_DIRS}" + ) + endif() +endif() diff --git a/lib/CMakeLists.txt b/lib/CMakeLists.txt index c002ab8..facead5 100644 --- a/lib/CMakeLists.txt +++ b/lib/CMakeLists.txt @@ -117,7 +117,7 @@ if(CHIAKI_ENABLE_SWITCH AND NOT CHIAKI_ENABLE_SWITCH_LINUX) endif() target_link_libraries(chiaki-lib protobuf-nanopb-static) -target_link_libraries(chiaki-lib jerasure) +target_link_libraries(chiaki-lib Jerasure::Jerasure) if(CHIAKI_LIB_ENABLE_OPUS) target_link_libraries(chiaki-lib ${Opus_LIBRARIES}) diff --git a/scripts/Dockerfile.bullseye b/scripts/Dockerfile.bullseye new file mode 100644 index 0000000..443c641 --- /dev/null +++ b/scripts/Dockerfile.bullseye @@ -0,0 +1,12 @@ + +FROM debian:bullseye + +RUN apt-get update +RUN apt-get -y install git g++ cmake pkg-config \ + libjerasure-dev libnanopb-dev libavcodec-dev libopus-dev \ + libssl-dev protobuf-compiler python3 python3-protobuf \ + libevdev-dev libudev-dev \ + qt5-default libqt5svg5-dev qtmultimedia5-dev libsdl2-dev + +CMD [] + diff --git a/scripts/ci-script b/scripts/ci-script new file mode 100755 index 0000000..e88acfb --- /dev/null +++ b/scripts/ci-script @@ -0,0 +1,9 @@ +#!/bin/bash + +set -e +set -x + +mkdir build && cd build +cmake .. -DCHIAKI_ENABLE_SETSU=ON "$@" +make -j8 +make test diff --git a/third-party/CMakeLists.txt b/third-party/CMakeLists.txt index b1357d5..14e7f28 100644 --- a/third-party/CMakeLists.txt +++ b/third-party/CMakeLists.txt @@ -10,39 +10,43 @@ set(NANOPB_SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/nanopb") set(NANOPB_SOURCE_DIR "${NANOPB_SOURCE_DIR}" PARENT_SCOPE) set(NANOPB_GENERATOR_PY "${NANOPB_SOURCE_DIR}/generator/nanopb_generator.py" PARENT_SCOPE) -################## -# gf-complete -################## +if(NOT CHIAKI_USE_SYSTEM_JERASURE) + ################## + # gf-complete + ################## -set(GF_COMPLETE_SOURCE - gf-complete/src/gf.c - gf-complete/src/gf_wgen.c - gf-complete/src/gf_w4.c - gf-complete/src/gf_w8.c - gf-complete/src/gf_w16.c - gf-complete/src/gf_w32.c - gf-complete/src/gf_w64.c - gf-complete/src/gf_w128.c - gf-complete/src/gf_rand.c - gf-complete/src/gf_general.c - gf-complete/src/gf_cpu.c) + set(GF_COMPLETE_SOURCE + gf-complete/src/gf.c + gf-complete/src/gf_wgen.c + gf-complete/src/gf_w4.c + gf-complete/src/gf_w8.c + gf-complete/src/gf_w16.c + gf-complete/src/gf_w32.c + gf-complete/src/gf_w64.c + gf-complete/src/gf_w128.c + gf-complete/src/gf_rand.c + gf-complete/src/gf_general.c + gf-complete/src/gf_cpu.c) -# TODO: support NEON + # TODO: support NEON -add_library(gf_complete STATIC ${GF_COMPLETE_SOURCE}) -target_include_directories(gf_complete PUBLIC gf-complete/include) + add_library(gf_complete STATIC ${GF_COMPLETE_SOURCE}) + target_include_directories(gf_complete PUBLIC gf-complete/include) -################## -# jerasure -################## + ################## + # jerasure + ################## -set(JERASURE_SOURCE - jerasure/src/galois.c - jerasure/src/jerasure.c - jerasure/src/reed_sol.c - jerasure/src/cauchy.c - jerasure/src/liberation.c) + set(JERASURE_SOURCE + jerasure/src/galois.c + jerasure/src/jerasure.c + jerasure/src/reed_sol.c + jerasure/src/cauchy.c + jerasure/src/liberation.c) -add_library(jerasure STATIC ${JERASURE_SOURCE}) -target_include_directories(jerasure PUBLIC jerasure/include) -target_link_libraries(jerasure gf_complete) + add_library(jerasure STATIC ${JERASURE_SOURCE}) + target_include_directories(jerasure PUBLIC jerasure/include) + target_link_libraries(jerasure gf_complete) + + add_library(Jerasure::Jerasure ALIAS jerasure) +endif() From e83cb040495bbf9959c8199ebb0b359e546ab5fb Mon Sep 17 00:00:00 2001 From: tuximail <49699523+tuximail@users.noreply.github.com> Date: Sat, 24 Oct 2020 15:46:50 +0200 Subject: [PATCH 051/237] Improve GUI CLI for streaming (#346) --- gui/include/streamsession.h | 3 +- gui/src/main.cpp | 72 +++++++++++++++++++++++++------------ gui/src/mainwindow.cpp | 2 +- gui/src/streamsession.cpp | 3 +- gui/src/streamwindow.cpp | 2 ++ 5 files changed, 57 insertions(+), 25 deletions(-) diff --git a/gui/include/streamsession.h b/gui/include/streamsession.h index 0790b48..831ed6b 100644 --- a/gui/include/streamsession.h +++ b/gui/include/streamsession.h @@ -58,8 +58,9 @@ struct StreamSessionConnectInfo QByteArray morning; ChiakiConnectVideoProfile video_profile; unsigned int audio_buffer_size; + bool fullscreen; - StreamSessionConnectInfo(Settings *settings, QString host, QByteArray regist_key, QByteArray morning); + StreamSessionConnectInfo(Settings *settings, QString host, QByteArray regist_key, QByteArray morning, bool fullscreen); }; class StreamSession : public QObject diff --git a/gui/src/main.cpp b/gui/src/main.cpp index f612b3d..4ec7871 100644 --- a/gui/src/main.cpp +++ b/gui/src/main.cpp @@ -84,11 +84,14 @@ int real_main(int argc, char *argv[]) QStringList cmds; cmds.append("stream"); + cmds.append("list"); #ifdef CHIAKI_ENABLE_CLI cmds.append(cli_commands.keys()); #endif parser.addPositionalArgument("command", cmds.join(", ")); + parser.addPositionalArgument("nickname", "Needed for stream command to get credentials for connecting. " + "Use 'list' to get the nickname."); parser.addPositionalArgument("host", "Address to connect to (when using the stream command)"); QCommandLineOption regist_key_option("registkey", "", "registkey"); @@ -97,44 +100,69 @@ int real_main(int argc, char *argv[]) QCommandLineOption morning_option("morning", "", "morning"); parser.addOption(morning_option); + QCommandLineOption fullscreen_option("fullscreen", "Start window in fullscreen (only for use with stream command)"); + parser.addOption(fullscreen_option); + parser.process(app); QStringList args = parser.positionalArguments(); if(args.length() == 0) return RunMain(app, &settings); + if(args[0] == "list") + { + for(const auto &host : settings.GetRegisteredHosts()) + printf("Host: %s \n", host.GetPS4Nickname().toLocal8Bit().constData()); + return 0; + } if(args[0] == "stream") { if(args.length() < 2) parser.showHelp(1); - QString host = args[1]; + //QString host = args[sizeof(args) -1]; //the ip is always the last param for stream + QString host = args[args.size()-1]; + QByteArray morning; + QByteArray regist_key; - if(parser.value(regist_key_option).isEmpty() || parser.value(morning_option).isEmpty()) - parser.showHelp(1); - - QByteArray regist_key = parser.value(regist_key_option).toUtf8(); - if(regist_key.length() > sizeof(ChiakiConnectInfo::regist_key)) + if(parser.value(regist_key_option).isEmpty() && parser.value(morning_option).isEmpty()) { - printf("Given regist key is too long (expected size <=%llu, got %d)\n", - (unsigned long long)sizeof(ChiakiConnectInfo::regist_key), - regist_key.length()); - return 1; + if(args.length() < 3) + parser.showHelp(1); + for(const auto &temphost : settings.GetRegisteredHosts()) + { + if(temphost.GetPS4Nickname() == args[1]) + { + morning = temphost.GetRPKey(); + regist_key = temphost.GetRPRegistKey(); + break; + } + printf("No configuration found for '%s'\n", args[1].toLocal8Bit().constData()); + return 1; + } } - regist_key += QByteArray(sizeof(ChiakiConnectInfo::regist_key) - regist_key.length(), 0); - - QByteArray morning = QByteArray::fromBase64(parser.value(morning_option).toUtf8()); - if(morning.length() != sizeof(ChiakiConnectInfo::morning)) + else { - printf("Given morning has invalid size (expected %llu, got %d)\n", - (unsigned long long)sizeof(ChiakiConnectInfo::morning), - morning.length()); - printf("Given morning has invalid size (expected %llu)", (unsigned long long)sizeof(ChiakiConnectInfo::morning)); - return 1; + regist_key = parser.value(regist_key_option).toUtf8(); + if(regist_key.length() > sizeof(ChiakiConnectInfo::regist_key)) + { + printf("Given regist key is too long (expected size <=%llu, got %d)\n", + (unsigned long long)sizeof(ChiakiConnectInfo::regist_key), + regist_key.length()); + return 1; + } + regist_key += QByteArray(sizeof(ChiakiConnectInfo::regist_key) - regist_key.length(), 0); + morning = QByteArray::fromBase64(parser.value(morning_option).toUtf8()); + if(morning.length() != sizeof(ChiakiConnectInfo::morning)) + { + printf("Given morning has invalid size (expected %llu, got %d)\n", + (unsigned long long)sizeof(ChiakiConnectInfo::morning), + morning.length()); + printf("Given morning has invalid size (expected %llu)", (unsigned long long)sizeof(ChiakiConnectInfo::morning)); + return 1; + } } - - StreamSessionConnectInfo connect_info(&settings, host, regist_key, morning); - + StreamSessionConnectInfo connect_info(&settings, host, regist_key, morning, parser.isSet(fullscreen_option)); return RunStream(app, connect_info); } #ifdef CHIAKI_ENABLE_CLI diff --git a/gui/src/mainwindow.cpp b/gui/src/mainwindow.cpp index c5fcc48..4148cea 100644 --- a/gui/src/mainwindow.cpp +++ b/gui/src/mainwindow.cpp @@ -263,7 +263,7 @@ void MainWindow::ServerItemWidgetTriggered() } QString host = server.GetHostAddr(); - StreamSessionConnectInfo info(settings, host, server.registered_host.GetRPRegistKey(), server.registered_host.GetRPKey()); + StreamSessionConnectInfo info(settings, host, server.registered_host.GetRPRegistKey(), server.registered_host.GetRPKey(), false); new StreamWindow(info); } else diff --git a/gui/src/streamsession.cpp b/gui/src/streamsession.cpp index ef3a62d..6ac6237 100644 --- a/gui/src/streamsession.cpp +++ b/gui/src/streamsession.cpp @@ -29,7 +29,7 @@ #define SETSU_UPDATE_INTERVAL_MS 4 -StreamSessionConnectInfo::StreamSessionConnectInfo(Settings *settings, QString host, QByteArray regist_key, QByteArray morning) +StreamSessionConnectInfo::StreamSessionConnectInfo(Settings *settings, QString host, QByteArray regist_key, QByteArray morning, bool fullscreen) : settings(settings) { key_map = settings->GetControllerMappingForDecoding(); @@ -41,6 +41,7 @@ StreamSessionConnectInfo::StreamSessionConnectInfo(Settings *settings, QString h this->regist_key = regist_key; this->morning = morning; audio_buffer_size = settings->GetAudioBufferSize(); + this->fullscreen = fullscreen; } static void AudioSettingsCb(uint32_t channels, uint32_t rate, void *user); diff --git a/gui/src/streamwindow.cpp b/gui/src/streamwindow.cpp index 678d40c..e73b798 100644 --- a/gui/src/streamwindow.cpp +++ b/gui/src/streamwindow.cpp @@ -38,6 +38,8 @@ StreamWindow::StreamWindow(const StreamSessionConnectInfo &connect_info, QWidget try { + if(connect_info.fullscreen) + showFullScreen(); Init(); } catch(const Exception &e) From f7c83e8416e61fc1e421f91f473191a5b47ae7c1 Mon Sep 17 00:00:00 2001 From: H0neyBadger Date: Sun, 25 Oct 2020 10:51:49 +0100 Subject: [PATCH 052/237] Add Nintendo Switch Borealis GUI (#349) --- .github/workflows/switch.yml | 12 +- .gitmodules | 3 + CMakeLists.txt | 30 +- README.md | 3 +- cmake/switch.cmake | 149 ++-- lib/CMakeLists.txt | 11 +- scripts/switch/build.sh | 22 +- scripts/switch/push-docker-build-chiaki.sh | 10 + scripts/switch/run-docker-build-chiaki.sh | 3 +- switch/CMakeLists.txt | 46 ++ switch/README.md | 64 ++ switch/include/discoverymanager.h | 61 ++ switch/include/exception.h | 35 + switch/include/gui.h | 102 +++ switch/include/host.h | 129 ++++ switch/include/io.h | 136 ++++ switch/include/settings.h | 105 +++ switch/res/add-24px.svg | 1 + switch/res/console.svg | 1 + switch/res/discover-24px.svg | 1 + switch/res/discover-off-24px.svg | 1 + switch/res/i18n/en-US | 1 + switch/res/icon.jpg | Bin 0 -> 25090 bytes switch/res/inter | 1 + switch/res/material | 1 + switch/res/settings-20px.svg | 1 + switch/src/discoverymanager.cpp | 259 +++++++ switch/src/gui.cpp | 514 +++++++++++++ switch/src/host.cpp | 287 ++++++++ switch/src/io.cpp | 797 +++++++++++++++++++++ switch/src/main.cpp | 175 +++++ switch/src/settings.cpp | 561 +++++++++++++++ third-party/CMakeLists.txt | 71 ++ third-party/borealis | 1 + 34 files changed, 3470 insertions(+), 124 deletions(-) create mode 100755 scripts/switch/push-docker-build-chiaki.sh create mode 100644 switch/CMakeLists.txt create mode 100644 switch/README.md create mode 100644 switch/include/discoverymanager.h create mode 100644 switch/include/exception.h create mode 100644 switch/include/gui.h create mode 100644 switch/include/host.h create mode 100644 switch/include/io.h create mode 100644 switch/include/settings.h create mode 120000 switch/res/add-24px.svg create mode 120000 switch/res/console.svg create mode 120000 switch/res/discover-24px.svg create mode 120000 switch/res/discover-off-24px.svg create mode 120000 switch/res/i18n/en-US create mode 100644 switch/res/icon.jpg create mode 120000 switch/res/inter create mode 120000 switch/res/material create mode 120000 switch/res/settings-20px.svg create mode 100644 switch/src/discoverymanager.cpp create mode 100644 switch/src/gui.cpp create mode 100644 switch/src/host.cpp create mode 100644 switch/src/io.cpp create mode 100644 switch/src/main.cpp create mode 100644 switch/src/settings.cpp create mode 160000 third-party/borealis diff --git a/.github/workflows/switch.yml b/.github/workflows/switch.yml index 7c5effe..b365ab6 100644 --- a/.github/workflows/switch.yml +++ b/.github/workflows/switch.yml @@ -1,5 +1,9 @@ name: Switch -on: [push] +on: + push: + branches: + - master + pull_request: jobs: build: @@ -13,4 +17,8 @@ jobs: git submodule update - name: Build Chiaki run: scripts/switch/run-docker-build-chiaki.sh - + - name: Upload chiaki.nro + uses: actions/upload-artifact@v1 + with: + name: chiaki.nro + path: ./build_switch/switch/chiaki.nro diff --git a/.gitmodules b/.gitmodules index 1d25e40..30d4edd 100644 --- a/.gitmodules +++ b/.gitmodules @@ -13,3 +13,6 @@ [submodule "android/app/src/main/cpp/oboe"] path = android/app/src/main/cpp/oboe url = https://github.com/google/oboe +[submodule "third-party/borealis"] + path = third-party/borealis + url = https://github.com/natinusala/borealis.git diff --git a/CMakeLists.txt b/CMakeLists.txt index f75bb09..1642cbd 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -40,17 +40,28 @@ include(CPack) list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake;${CMAKE_CURRENT_SOURCE_DIR}/setsu/cmake") # configure nintendo switch toolchain -if(CHIAKI_ENABLE_SWITCH AND CHIAKI_ENABLE_SWITCH_LINUX) - # CHIAKI_ENABLE_SWITCH_LINUX is a special testing version - # the aim is to troubleshoot nitendo switch chiaki verison - # from a x86 linux os - add_definitions(-DCHIAKI_ENABLE_SWITCH_LINUX) - set(CMAKE_BUILD_TYPE Debug) -elseif(CHIAKI_ENABLE_SWITCH) - add_definitions(-D__SWITCH__) +if(CHIAKI_ENABLE_SWITCH) + # load switch.cmake toolchain form ./cmake folder + # include(switch) + # to compile borealis third party + set(CMAKE_CXX_STANDARD 17) # TODO check if android ... or other versions are enabled # force mbedtls as crypto lib set(CHIAKI_LIB_ENABLE_MBEDTLS ON) + + if(CHIAKI_SWITCH_ENABLE_LINUX) + # CHIAKI_SWITCH_ENABLE_LINUX is a special testing version + # the aim is to troubleshoot nitendo switch chiaki verison + # from a x86 linux os + add_definitions(-DCHIAKI_SWITCH_ENABLE_LINUX) + add_definitions(-DBOREALIS_RESOURCES="./switch/res/") + set(CMAKE_BUILD_TYPE Debug) + add_definitions(-DDEBUG_OPENGL) + else() + add_definitions(-D__SWITCH__) + # use symbolic links to load font .. + add_definitions(-DBOREALIS_RESOURCES="romfs:/") + endif() endif() if(CHIAKI_USE_SYSTEM_JERASURE) @@ -120,6 +131,5 @@ if(CHIAKI_ENABLE_ANDROID) endif() if(CHIAKI_ENABLE_SWITCH) - #TODO - #add_subdirectory(switch) + add_subdirectory(switch) endif() diff --git a/README.md b/README.md index 0831ecf..e710b7c 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ [![Travis Build Status](https://travis-ci.com/thestr4ng3r/chiaki.svg?branch=master)](https://travis-ci.com/thestr4ng3r/chiaki) [![AppVeyor Build status](https://ci.appveyor.com/api/projects/status/dnj9cmx5mmaaawer?svg=true)](https://ci.appveyor.com/project/thestr4ng3r/chiaki) [![builds.sr.ht Status](https://builds.sr.ht/~thestr4ng3r/chiaki.svg)](https://builds.sr.ht/~thestr4ng3r/chiaki?) Chiaki is a Free and Open Source Software Client for PlayStation 4 Remote Play -for Linux, FreeBSD, OpenBSD, Android, macOS, Windows and potentially even more platforms. +for Linux, FreeBSD, OpenBSD, Android, macOS, Windows, Nintendo Switch and potentially even more platforms. ![Screenshot](assets/screenshot.png) @@ -37,6 +37,7 @@ You can download them [here](https://github.com/thestr4ng3r/chiaki/releases). * **Android**: Install from [Google Play](https://play.google.com/store/apps/details?id=com.metallic.chiaki), [F-Droid](https://f-droid.org/packages/com.metallic.chiaki/) or download the APK from GitHub. * **macOS**: Drag the application from the `.dmg` into your Applications folder. * **Windows**: Extract the `.zip` file and execute `chiaki.exe`. +* **Switch**: Follow README specific [instructions](./switch/README.md) ### Building from Source diff --git a/cmake/switch.cmake b/cmake/switch.cmake index e9a07e5..05c18b9 100644 --- a/cmake/switch.cmake +++ b/cmake/switch.cmake @@ -1,116 +1,79 @@ -# https://github.com/nxengine/nxengine-evo - # Find DEVKITPRO -if(NOT DEFINED ENV{DEVKITPRO}) - message(FATAL_ERROR "You must have defined DEVKITPRO before calling cmake.") +if(NOT DEFINED ENV{DEVKITPRO} OR NOT DEFINED ENV{PORTLIBS_PREFIX}) + message(FATAL_ERROR "Please set DEVKITPRO & PORTLIBS_PREFIX env before calling cmake. https://devkitpro.org/wiki/Getting_Started") endif() -set(DEVKITPRO $ENV{DEVKITPRO}) +set(DEVKITPRO "$ENV{DEVKITPRO}") +set(PORTLIBS_PREFIX "$ENV{PORTLIBS_PREFIX}") -function(switchvar cmakevar var default) - # read or set env var - if(NOT DEFINED "ENV{$var}") - set("ENV{$var}" default) - endif() - set("$cmakevar" "ENV{$var}") -endfunction() +# include devkitpro toolchain +include("${DEVKITPRO}/switch.cmake") -# allow gcc -g to use -# aarch64-none-elf-addr2line -e build_switch/switch/chiaki -f -p -C -a 0xCCB5C -set(CMAKE_BUILD_TYPE Debug) +# Enable gcc -g, to use +# /opt/devkitpro/devkitA64/bin/aarch64-none-elf-addr2line -e build_switch/switch/chiaki -f -p -C -a 0xCCB5C +# set(CMAKE_BUILD_TYPE Debug) +# set(CMAKE_POSITION_INDEPENDENT_CODE ON) +# set(BUILD_SHARED_LIBS OFF CACHE INTERNAL "Shared libs not available" ) -set( TOOL_OS_SUFFIX "" ) -if( CMAKE_HOST_WIN32 ) - set( TOOL_OS_SUFFIX ".exe" ) -endif() +# FIXME rework this file to use the toolchain only +# https://github.com/diasurgical/devilutionX/pull/764 +set(ARCH "-march=armv8-a+crc+crypto -mtune=cortex-a57 -mtp=soft -ftls-model=local-exec") +# set(CMAKE_C_FLAGS "-O2 -ffunction-sections ${ARCH}") +set(CMAKE_CXX_FLAGS "${CMAKE_C_FLAGS}") +# workaroud force -fPIE to avoid +# aarch64-none-elf/bin/ld: read-only segment has dynamic relocations +set(CMAKE_EXE_LINKER_FLAGS "-specs=${DEVKITPRO}/libnx/switch.specs ${ARCH} -fPIE -Wl,-Map,Output.map") -set(CMAKE_SYSTEM_PROCESSOR "armv8-a") -set(CMAKE_C_COMPILER "${DEVKITPRO}/devkitA64/bin/aarch64-none-elf-gcc${TOOL_OS_SUFFIX}" CACHE PATH "C compiler") -set(CMAKE_CXX_COMPILER "${DEVKITPRO}/devkitA64/bin/aarch64-none-elf-g++${TOOL_OS_SUFFIX}" CACHE PATH "C++ compiler") -set(CMAKE_ASM_COMPILER "${DEVKITPRO}/devkitA64/bin/aarch64-none-elf-as${TOOL_OS_SUFFIX}" CACHE PATH "C++ compiler") -set(CMAKE_STRIP "${DEVKITPRO}/devkitA64/bin/aarch64-none-elf-strip${TOOL_OS_SUFFIX}" CACHE PATH "strip") -set(CMAKE_AR "${DEVKITPRO}/devkitA64/bin/aarch64-none-elf-ar${TOOL_OS_SUFFIX}" CACHE PATH "archive") -set(CMAKE_LINKER "${DEVKITPRO}/devkitA64/bin/aarch64-none-elf-ld${TOOL_OS_SUFFIX}" CACHE PATH "linker") -set(CMAKE_NM "${DEVKITPRO}/devkitA64/bin/aarch64-none-elf-nm${TOOL_OS_SUFFIX}" CACHE PATH "nm") -set(CMAKE_OBJCOPY "${DEVKITPRO}/devkitA64/bin/aarch64-none-elf-objcopy${TOOL_OS_SUFFIX}" CACHE PATH "objcopy") -set(CMAKE_OBJDUMP "${DEVKITPRO}/devkitA64/bin/aarch64-none-elf-objdump${TOOL_OS_SUFFIX}" CACHE PATH "objdump") -set(CMAKE_RANLIB "${DEVKITPRO}/devkitA64/bin/aarch64-none-elf-ranlib${TOOL_OS_SUFFIX}" CACHE PATH "ranlib") +# add portlibs to the list of include dir +include_directories("${PORTLIBS_PREFIX}/include") -# custom /opt/devkitpro/switchvars.sh -switchvar(PORTLIBS_PREFIX PORTLIBS_PREFIX "${DEVKITPRO}/portlibs/switch") -switchvar(ARCH ARCH "-march=armv8-a+crc+crypto -mtune=cortex-a57 -mtp=soft -fPIC -ftls-model=local-exec") -switchvar(CMAKE_C_FLAGS CFLAGS "${ARCH} -O2 -ffunction-sections -fdata-sections") -switchvar(CMAKE_CXX_FLAGS CXXFLAGS "${CMAKE_C_FLAGS}") -switchvar(CMAKE_CPP_FLAGS CPPFLAGS "-D__SWITCH__ -I${PORTLIBS_PREFIX}/include -isystem${DEVKITPRO}/libnx/include") -switchvar(CMAKE_LD_FLAGS LDFLAGS "${ARCH} -L${PORTLIBS_PREFIX}/lib -L${DEVKITPRO}/libnx/lib") -switchvar(LIBS LIBS "-lnx") - - - -# cache flags -set( CMAKE_CXX_FLAGS "" CACHE STRING "c++ flags" ) -set( CMAKE_C_FLAGS "" CACHE STRING "c flags" ) -set( CMAKE_CXX_FLAGS_RELEASE "-O3 -DNDEBUG" CACHE STRING "c++ Release flags" ) -set( CMAKE_C_FLAGS_RELEASE "-O3 -DNDEBUG" CACHE STRING "c Release flags" ) -set( CMAKE_CXX_FLAGS_DEBUG "-O0 -g -DDEBUG -D_DEBUG" CACHE STRING "c++ Debug flags" ) -set( CMAKE_C_FLAGS_DEBUG "-O0 -g -DDEBUG -D_DEBUG" CACHE STRING "c Debug flags" ) -set( CMAKE_SHARED_LINKER_FLAGS "" CACHE STRING "shared linker flags" ) -set( CMAKE_MODULE_LINKER_FLAGS "" CACHE STRING "module linker flags" ) -set( CMAKE_EXE_LINKER_FLAGS "-mtp=soft -fPIE -L${DEVKITPRO}/portlibs/switch/lib -L${DEVKITPRO}/libnx/lib -specs=${DEVKITPRO}/libnx/switch.specs -g" CACHE STRING "executable linker flags" ) - -# we require the relocation table -set(CMAKE_C_FLAGS "-I/opt/devkitpro/libnx/include -D__SWITCH__ -march=armv8-a -mtune=cortex-a57 -mtp=soft -ffunction-sections -fdata-sections -fPIE") -set(CMAKE_CXX_FLAGS "${CMAKE_C_FLAGS} -fno-rtti") - -# switchvar(CMAKE_CXX_FLAGS CXXFLAGS "${CMAKE_C_FLAGS} -fno-rtti") -include_directories(${DEVKITPRO}/libnx/include ${PORTLIBS_PREFIX}/include) - -# where is the target environment -set(CMAKE_FIND_ROOT_PATH - ${DEVKITPRO}/devkitA64 - ${DEVKITPRO}/libnx - ${DEVKITPRO}/portlibs/switch) - -# only search for libraries and includes in toolchain -set( CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY ) -set( CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY ) +# troubleshoot +message(STATUS "CMAKE_FIND_ROOT_PATH = ${CMAKE_FIND_ROOT_PATH}") +message(STATUS "PKG_CONFIG_EXECUTABLE = ${PKG_CONFIG_EXECUTABLE}") +message(STATUS "CMAKE_EXE_LINKER_FLAGS = ${CMAKE_EXE_LINKER_FLAGS}") +get_property(include_directories DIRECTORY PROPERTY INCLUDE_DIRECTORIES) +message(STATUS "INCLUDE_DIRECTORIES = ${include_directories}") +message(STATUS "CMAKE_C_COMPILER = ${CMAKE_C_COMPILER}") +message(STATUS "CMAKE_CXX_COMPILER = ${CMAKE_CXX_COMPILER}") find_program(ELF2NRO elf2nro ${DEVKITPRO}/tools/bin) if (ELF2NRO) message(STATUS "elf2nro: ${ELF2NRO} - found") -else () - message(WARNING "elf2nro - not found") -endif () +else() + message(WARNING "elf2nro - not found") +endif() find_program(NACPTOOL nacptool ${DEVKITPRO}/tools/bin) if (NACPTOOL) - message(STATUS "nacptool: ${NACPTOOL} - found") -else () - message(WARNING "nacptool - not found") -endif () + message(STATUS "nacptool: ${NACPTOOL} - found") +else() + message(WARNING "nacptool - not found") +endif() function(__add_nacp target APP_TITLE APP_AUTHOR APP_VERSION) - set(__NACP_COMMAND ${NACPTOOL} --create ${APP_TITLE} ${APP_AUTHOR} ${APP_VERSION} ${CMAKE_CURRENT_BINARY_DIR}/${target}) + set(__NACP_COMMAND ${NACPTOOL} --create ${APP_TITLE} ${APP_AUTHOR} ${APP_VERSION} ${CMAKE_CURRENT_BINARY_DIR}/${target}) - add_custom_command(OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/${target} - COMMAND ${__NACP_COMMAND} - WORKING_DIRECTORY ${CMAKE_CURRENT_LIST_DIR} - VERBATIM - ) + add_custom_command(OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/${target} + COMMAND ${__NACP_COMMAND} + WORKING_DIRECTORY ${CMAKE_CURRENT_LIST_DIR} + VERBATIM + ) endfunction() function(add_nro_target target title author version icon romfs) - get_filename_component(target_we ${target} NAME_WE) - if (NOT ${target_we}.nacp) - __add_nacp(${target_we}.nacp ${title} ${author} ${version}) - endif () - add_custom_command(OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/${target_we}.nro - COMMAND ${ELF2NRO} $ ${CMAKE_CURRENT_BINARY_DIR}/${target_we}.nro --icon=${icon} --nacp=${CMAKE_CURRENT_BINARY_DIR}/${target_we}.nacp - # --romfsdir=${romfs} - DEPENDS ${target} ${CMAKE_CURRENT_BINARY_DIR}/${target_we}.nacp - VERBATIM - ) - add_custom_target(${target_we}_nro ALL SOURCES ${CMAKE_CURRENT_BINARY_DIR}/${target_we}.nro) + get_filename_component(target_we ${target} NAME_WE) + if(NOT ${target_we}.nacp) + __add_nacp(${target_we}.nacp ${title} ${author} ${version}) + endif() + add_custom_command(OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/${target_we}.nro + COMMAND ${ELF2NRO} $ + ${CMAKE_CURRENT_BINARY_DIR}/${target_we}.nro + --icon=${icon} + --nacp=${CMAKE_CURRENT_BINARY_DIR}/${target_we}.nacp + --romfsdir=${romfs} + DEPENDS ${target} ${CMAKE_CURRENT_BINARY_DIR}/${target_we}.nacp + VERBATIM + ) + add_custom_target(${target_we}_nro ALL SOURCES ${CMAKE_CURRENT_BINARY_DIR}/${target_we}.nro) endfunction() - diff --git a/lib/CMakeLists.txt b/lib/CMakeLists.txt index facead5..f7083eb 100644 --- a/lib/CMakeLists.txt +++ b/lib/CMakeLists.txt @@ -101,8 +101,10 @@ target_link_libraries(chiaki-lib Threads::Threads) if(CHIAKI_LIB_ENABLE_MBEDTLS) # provided by mbedtls-static (mbedtls-devel) - # find_package(mbedcrypto REQUIRED) - target_link_libraries(chiaki-lib mbedtls mbedx509 mbedcrypto) + find_library(MBEDTLS mbedtls) + find_library(MBEDX509 mbedx509) + find_library(MBEDCRYPTO mbedcrypto) + target_link_libraries(chiaki-lib ${MBEDTLS} ${MBEDX509} ${MBEDCRYPTO}) elseif(CHIAKI_LIB_OPENSSL_EXTERNAL_PROJECT) target_link_libraries(chiaki-lib OpenSSL_Crypto) else() @@ -111,11 +113,6 @@ else() target_link_libraries(chiaki-lib OpenSSL::Crypto) endif() -if(CHIAKI_ENABLE_SWITCH AND NOT CHIAKI_ENABLE_SWITCH_LINUX) - # to provides csrngGetRandomBytes - target_link_libraries(chiaki-lib nx) -endif() - target_link_libraries(chiaki-lib protobuf-nanopb-static) target_link_libraries(chiaki-lib Jerasure::Jerasure) diff --git a/scripts/switch/build.sh b/scripts/switch/build.sh index bd351f5..bbcb729 100755 --- a/scripts/switch/build.sh +++ b/scripts/switch/build.sh @@ -3,15 +3,14 @@ set -xveo pipefail arg1=$1 -CHIAKI_ENABLE_SWITCH_LINUX="ON" +CHIAKI_SWITCH_ENABLE_LINUX="ON" build="./build" if [ "$arg1" != "linux" ]; then - CHIAKI_ENABLE_SWITCH_LINUX="OFF" - source /opt/devkitpro/switchvars.sh - toolchain=../cmake/switch.cmake # TODO: devkitpro ships a toolchain in /opt/devkitpro/switch.cmake, but it's broken. - - export CC=${TOOL_PREFIX}gcc - export CXX=${TOOL_PREFIX}g++ + CHIAKI_SWITCH_ENABLE_LINUX="OFF" + # source /opt/devkitpro/switchvars.sh + # toolchain="${DEVKITPRO}/switch.cmake" + toolchain="cmake/switch.cmake" + export PORTLIBS_PREFIX="$(${DEVKITPRO}/portlibs_prefix.sh switch)" build="./build_switch" fi @@ -22,14 +21,17 @@ build_chiaki (){ pushd "${BASEDIR}" #rm -rf ./build - cmake -B "${build}" -DCMAKE_TOOLCHAIN_FILE=${toolchain} \ + cmake -B "${build}" \ + -DCMAKE_TOOLCHAIN_FILE=${toolchain} \ -DCHIAKI_ENABLE_TESTS=OFF \ -DCHIAKI_ENABLE_CLI=OFF \ -DCHIAKI_ENABLE_GUI=OFF \ -DCHIAKI_ENABLE_ANDROID=OFF \ -DCHIAKI_ENABLE_SWITCH=ON \ - -DCHIAKI_ENABLE_SWITCH_LINUX="${CHIAKI_ENABLE_SWITCH_LINUX}" \ - -DCHIAKI_LIB_ENABLE_MBEDTLS=ON + -DCHIAKI_SWITCH_ENABLE_LINUX="${CHIAKI_SWITCH_ENABLE_LINUX}" \ + -DCHIAKI_LIB_ENABLE_MBEDTLS=ON \ + # -DCMAKE_VERBOSE_MAKEFILE:BOOL=ON \ + # -DCMAKE_FIND_DEBUG_MODE=ON pushd "${BASEDIR}/${build}" make -j8 diff --git a/scripts/switch/push-docker-build-chiaki.sh b/scripts/switch/push-docker-build-chiaki.sh new file mode 100755 index 0000000..ff66e40 --- /dev/null +++ b/scripts/switch/push-docker-build-chiaki.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +cd "`dirname $(readlink -f ${0})`/../.." + +docker run \ + -v "`pwd`:/build/chiaki" \ + -p 28771:28771 -ti \ + chiaki-switch \ + -c "/opt/devkitpro/tools/bin/nxlink -a $@ -s /build/chiaki/build_switch/switch/chiaki.nro" + diff --git a/scripts/switch/run-docker-build-chiaki.sh b/scripts/switch/run-docker-build-chiaki.sh index 0fe7ad6..c81c788 100755 --- a/scripts/switch/run-docker-build-chiaki.sh +++ b/scripts/switch/run-docker-build-chiaki.sh @@ -4,7 +4,8 @@ cd "`dirname $(readlink -f ${0})`/../.." docker run \ -v "`pwd`:/build/chiaki" \ + -w "/build/chiaki" \ -t \ thestr4ng3r/chiaki-build-switch \ - -c "cd /build/chiaki && scripts/switch/build.sh" + -c "scripts/switch/build.sh" diff --git a/switch/CMakeLists.txt b/switch/CMakeLists.txt new file mode 100644 index 0000000..62fa146 --- /dev/null +++ b/switch/CMakeLists.txt @@ -0,0 +1,46 @@ + +set(CMAKE_INCLUDE_CURRENT_DIR ON) + +find_package(FFMPEG REQUIRED COMPONENTS avcodec avutil swscale) +find_library(SDL2 SDL2) +find_library(SWRESAMPLE swresample) + +# find -type f | grep -P '\.(h|cpp)$' | sed 's#\./#\t\t#g' +add_executable(chiaki WIN32 + src/discoverymanager.cpp + src/settings.cpp + src/io.cpp + src/host.cpp + src/main.cpp + src/gui.cpp) + +target_include_directories(chiaki PRIVATE include) + +target_link_libraries(chiaki + chiaki-lib + borealis + ${SDL2} + FFMPEG::avcodec + FFMPEG::avutil + FFMPEG::swscale + ${SWRESAMPLE} + ${SWSCALE}) + +# OS links +if(CHIAKI_SWITCH_ENABLE_LINUX) + target_link_libraries(chiaki dl) +else() + # libnx is forced by the switch toolchain + find_library(Z z) + find_library(GLAPI glapi) + find_library(DRM_NOUVEAU drm_nouveau) + target_link_libraries(chiaki ${Z} ${GLAPI} ${DRM_NOUVEAU}) +endif() + +install(TARGETS chiaki + RUNTIME DESTINATION bin + BUNDLE DESTINATION bin) + +if(CHIAKI_ENABLE_SWITCH AND NOT CHIAKI_SWITCH_ENABLE_LINUX) + add_nro_target(chiaki "chiaki" "Chiaki team" "${CHIAKI_VERSION}" "${CMAKE_SOURCE_DIR}/switch/res/icon.jpg" "${CMAKE_SOURCE_DIR}/switch/res") +endif() diff --git a/switch/README.md b/switch/README.md new file mode 100644 index 0000000..e536d9b --- /dev/null +++ b/switch/README.md @@ -0,0 +1,64 @@ +Nintendo Switch build instructions +================================== +this project requires the devkitpro toolchain. +you can use your personal computer to install devkitpro +but the easiest way is to use the following container. + +Build container image +--------------------- +``` +bash scripts/switch/build-docker-image.sh +``` + +Run container +------------- +from the project's [root folder](../) +``` +docker run -it --rm \ + -v "$(pwd):/build" \ + -p 28771:28771 \ + chiaki-switch +``` + +Build Project +------------- +``` +bash scripts/switch/run-docker-build-chiaki.sh +``` + +tools +----- +Push to homebrew Netloader +``` +# where X.X.X.X is the IP of your switch +scripts/switch/push-docker-build-chiaki.sh 192.168.0.200 +``` + +Troubleshoot +``` +# replace 0xCCB5C with the backtrace adress (PC - Backtrace Start Address) +aarch64-none-elf-addr2line \ + -e ./build_switch/switch/chiaki \ + -f -p -C -a 0xCCB5C +``` + +Chiaki config file +------------------ +The **chiaki.conf** is generated by the application. +this file contains sensitive data. (do not share this file) +```ini +# required: PS4-XXX (PS4 local name) +# name from PS4 Settings > System > system information +[PS4-XXX] +# required: lan PS4 IP address +# IP from PS4 Settings > System > system information +host_ip = X.X.X.X +# required: sony oline id (login) +psn_online_id = ps4_online_id +# required (PS4>7.0 Only): https://github.com/thestr4ng3r/chiaki#obtaining-your-psn-accountid +psn_account_id = ps4_base64_account_id +# optional(default 60): remote play fps (must be 30 or 60) +video_fps = 60 +# optional(default 720p): remote play resolution (must be 720p, 540p or 360p) +video_resolution = 720p +``` diff --git a/switch/include/discoverymanager.h b/switch/include/discoverymanager.h new file mode 100644 index 0000000..420eef1 --- /dev/null +++ b/switch/include/discoverymanager.h @@ -0,0 +1,61 @@ +/* + * This file is part of Chiaki. + * + * Chiaki is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Chiaki is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Chiaki. If not, see . + */ + +#ifndef CHIAKI_DISCOVERYMANAGER_H +#define CHIAKI_DISCOVERYMANAGER_H + +#include +#include + +#include +#include + +#include "host.h" +#include "settings.h" + +static void Discovery(ChiakiDiscoveryHost*, void*); + +class DiscoveryManager +{ + private: + Settings * settings; + ChiakiLog * log; + ChiakiDiscoveryService service; + ChiakiDiscovery discovery; + struct sockaddr * host_addr; + size_t host_addr_len; + uint32_t GetIPv4BroadcastAddr(); + bool service_enable; + public: + typedef enum hoststate + { + UNKNOWN, + READY, + STANDBY, + SHUTDOWN, + } HostState; + + DiscoveryManager(Settings *settings); + ~DiscoveryManager(); + void SetService(bool); + int Send(); + int Send(struct sockaddr *host_addr, size_t host_addr_len); + int Send(const char *); + void DiscoveryCB(ChiakiDiscoveryHost*); +}; + +#endif //CHIAKI_DISCOVERYMANAGER_H diff --git a/switch/include/exception.h b/switch/include/exception.h new file mode 100644 index 0000000..a971b03 --- /dev/null +++ b/switch/include/exception.h @@ -0,0 +1,35 @@ +/* + * This file is part of Chiaki. + * + * Chiaki is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Chiaki is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Chiaki. If not, see . + */ + +#ifndef CHIAKI_EXCEPTION_H +#define CHIAKI_EXCEPTION_H + +#include + +#include + +class Exception : public std::exception +{ + private: + const char *msg; + + public: + explicit Exception(const char *msg) : msg(msg) {} + const char *what() const noexcept override { return msg; } +}; + +#endif // CHIAKI_EXCEPTION_H diff --git a/switch/include/gui.h b/switch/include/gui.h new file mode 100644 index 0000000..378fb2e --- /dev/null +++ b/switch/include/gui.h @@ -0,0 +1,102 @@ +/* + * This file is part of Chiaki. + * + * Chiaki is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Chiaki is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Chiaki. If not, see . + */ + +#ifndef CHIAKI_GUI_H +#define CHIAKI_GUI_H + +#include +#include +#include "nanovg.h" +#include "nanovg_gl.h" +#include "nanovg_gl_utils.h" + +#include + +#include +#include + +#include "host.h" +#include "settings.h" +#include "discoverymanager.h" +#include "io.h" + +class HostInterface +{ + private: + brls::TabFrame * root; + IO * io; + Host * host; + Settings * settings; + brls::List * hostList; + bool connected = false; + public: + HostInterface(brls::List * hostList, IO * io, Host * host, Settings * settings); + ~HostInterface(); + + void Register(bool pin_incorrect); + void Wakeup(brls::View * view); + void Connect(brls::View * view); + void ConnectSession(); + void Disconnect(); + bool Stream(); + bool CloseStream(ChiakiQuitEvent * quit); +}; + +class MainApplication +{ + private: + ChiakiLog * log; + std::map * hosts; + Settings * settings; + DiscoveryManager * discoverymanager; + IO * io; + brls::TabFrame * rootFrame; + std::map host_menuitems; + bool BuildConfigurationMenu(brls::List *, Host * host = nullptr); + public: + MainApplication(std::map * hosts, + Settings * settings, DiscoveryManager * discoverymanager, + IO * io, ChiakiLog * log); + + ~MainApplication(); + bool Load(); +}; + +class PS4RemotePlay : public brls::View +{ + private: + brls::AppletFrame * frame; + // to display stream on screen + IO * io; + // to send gamepad inputs + Host * host; + brls::Label * label; + ChiakiControllerState state = { 0 }; + // FPS calculation + // double base_time; + // int frame_counter = 0; + // int fps = 0; + + public: + PS4RemotePlay(IO * io, Host * host); + ~PS4RemotePlay(); + + void draw(NVGcontext * vg, int x, int y, unsigned width, unsigned height, brls::Style * style, brls::FrameContext * ctx) override; +}; + +#endif // CHIAKI_GUI_H + diff --git a/switch/include/host.h b/switch/include/host.h new file mode 100644 index 0000000..2a8c5e3 --- /dev/null +++ b/switch/include/host.h @@ -0,0 +1,129 @@ +/* + * This file is part of Chiaki. + * + * Chiaki is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Chiaki is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Chiaki. If not, see . + */ + +#ifndef CHIAKI_HOST_H +#define CHIAKI_HOST_H + +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "exception.h" +#include "io.h" +#include "settings.h" + +class DiscoveryManager; +static void Discovery(ChiakiDiscoveryHost*, void *); +static void InitAudioCB(unsigned int channels, unsigned int rate, void * user); +static bool VideoCB(uint8_t * buf, size_t buf_size, void * user); +static void AudioCB(int16_t * buf, size_t samples_count, void * user); +static void RegistEventCB(ChiakiRegistEvent * event, void * user); +static void EventCB(ChiakiEvent * event, void * user); + +enum RegistError +{ + HOST_REGISTER_OK, + HOST_REGISTER_ERROR_SETTING_PSNACCOUNTID, + HOST_REGISTER_ERROR_SETTING_PSNONLINEID +}; + +class Settings; + +class Host +{ + private: + ChiakiLog * log = nullptr; + Settings * settings = nullptr; + //video config + ChiakiVideoResolutionPreset video_resolution = CHIAKI_VIDEO_RESOLUTION_PRESET_720p; + ChiakiVideoFPSPreset video_fps = CHIAKI_VIDEO_FPS_PRESET_60; + // info from discovery manager + int device_discovery_protocol_version = 0; + std::string host_type; + // user info + std::string psn_online_id = ""; + std::string psn_account_id = ""; + // info from regist/settings + ChiakiRegist regist = {}; + ChiakiRegistInfo regist_info = {}; + std::function chiaki_regist_event_type_finished_canceled = nullptr; + std::function chiaki_regist_event_type_finished_failed = nullptr; + std::function chiaki_regist_event_type_finished_success = nullptr; + + std::string ap_ssid; + std::string ap_bssid; + std::string ap_key; + std::string ap_name; + std::string ps4_nickname; + // mac address = 48 bits + uint8_t ps4_mac[6] = {0}; + char rp_regist_key[CHIAKI_SESSION_AUTH_SIZE] = {0}; + uint32_t rp_key_type = 0; + uint8_t rp_key[0x10] = {0}; + // manage stream session + bool session_init = false; + ChiakiSession session; + ChiakiOpusDecoder opus_decoder; + ChiakiConnectVideoProfile video_profile; + ChiakiControllerState keyboard_state; + friend class DiscoveryManager; + friend class Settings; + public: + // internal state + ChiakiDiscoveryHostState state = CHIAKI_DISCOVERY_HOST_STATE_UNKNOWN; + bool discovered = false; + bool registered = false; + // rp_key_data is true when rp_key, rp_regist_key, rp_key_type + bool rp_key_data = false; + std::string host_name; + int system_version = 0; + // sony's host_id == mac addr without colon + std::string host_id; + std::string host_addr; + Host(ChiakiLog * log, Settings * settings, std::string host_name); + ~Host(); + bool GetVideoResolution(int * ret_width, int * ret_height); + int Register(std::string pin); + int Wakeup(); + int InitSession(IO *); + int FiniSession(); + void StopSession(); + void StartSession(); + void SendFeedbackState(ChiakiControllerState*); + void RegistCB(ChiakiRegistEvent *); + + void SetRegistEventTypeFinishedCanceled(std::function chiaki_regist_event_type_finished_canceled) + { + this->chiaki_regist_event_type_finished_canceled = chiaki_regist_event_type_finished_canceled; + }; + void SetRegistEventTypeFinishedFailed(std::function chiaki_regist_event_type_finished_failed) + { + this->chiaki_regist_event_type_finished_failed = chiaki_regist_event_type_finished_failed; + }; + void SetRegistEventTypeFinishedSuccess(std::function chiaki_regist_event_type_finished_success) + { + this->chiaki_regist_event_type_finished_success = chiaki_regist_event_type_finished_success; + }; +}; + +#endif diff --git a/switch/include/io.h b/switch/include/io.h new file mode 100644 index 0000000..69ac90f --- /dev/null +++ b/switch/include/io.h @@ -0,0 +1,136 @@ +/* + * This file is part of Chiaki. + * + * Chiaki is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Chiaki is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Chiaki. If not, see . + */ + +#ifndef CHIAKI_IO_H +#define CHIAKI_IO_H + +#include +#include +#include + +#include // glad library (OpenGL loader) + +#include + + +/* +https://github.com/devkitPro/switch-glad/blob/master/include/glad/glad.h +https://glad.dav1d.de/#profile=core&language=c&specification=gl&api=gl%3D4.3&extensions=GL_EXT_texture_compression_s3tc&extensions=GL_EXT_texture_filter_anisotropic + +Language/Generator: C/C++ +Specification: gl +APIs: gl=4.3 +Profile: core +Extensions: +GL_EXT_texture_compression_s3tc, +GL_EXT_texture_filter_anisotropic +Loader: False +Local files: False +Omit khrplatform: False +Reproducible: False +*/ + +#include +extern "C" +{ +#include +#include +#include +} + +#include +#include + +#include "exception.h" + +#define PLANES_COUNT 3 +#define SDL_JOYSTICK_COUNT 2 + +class IO +{ + private: + ChiakiLog * log; + int video_width; + int video_height; + bool quit = false; + // opengl reader writer + std::mutex mtx; + // default nintendo switch res + int screen_width = 1280; + int screen_height = 720; + std::function chiaki_event_connected_cb = nullptr; + std::function chiaki_even_login_pin_request_cb = nullptr; + std::function chiaki_event_quit_cb = nullptr; + AVCodec * codec; + AVCodecContext * codec_context; + AVFrame * frame; + SDL_AudioDeviceID sdl_audio_device_id = 0; + SDL_Event sdl_event; + SDL_Joystick * sdl_joystick_ptr[SDL_JOYSTICK_COUNT] = {0}; + GLuint vao; + GLuint vbo; + GLuint tex[PLANES_COUNT]; + GLuint pbo[PLANES_COUNT]; + GLuint vert; + GLuint frag; + GLuint prog; + private: + bool InitAVCodec(); + bool InitOpenGl(); + bool InitOpenGlTextures(); + bool InitOpenGlShader(); + void OpenGlDraw(); +#ifdef DEBUG_OPENGL + void CheckGLError(const char * func, const char * file, int line); + void DumpShaderError(GLuint prog, const char * func, const char * file, int line); + void DumpProgramError(GLuint prog, const char * func, const char * file, int line); +#endif + GLuint CreateAndCompileShader(GLenum type, const char * source); + void SetOpenGlYUVPixels(AVFrame * frame); + bool ReadGameKeys(SDL_Event * event, ChiakiControllerState * state); + bool ReadGameTouchScreen(ChiakiControllerState * state); + public: + IO(ChiakiLog * log); + ~IO(); + void SetMesaConfig(); + bool VideoCB(uint8_t * buf, size_t buf_size); + void SetEventConnectedCallback(std::function chiaki_event_connected_cb) + { + this->chiaki_event_connected_cb = chiaki_event_connected_cb; + }; + void SetEventLoginPinRequestCallback(std::function chiaki_even_login_pin_request_cb) + { + this->chiaki_even_login_pin_request_cb = chiaki_even_login_pin_request_cb; + }; + void SetEventQuitCallback(std::function chiaki_event_quit_cb) + { + this->chiaki_event_quit_cb = chiaki_event_quit_cb; + }; + void InitAudioCB(unsigned int channels, unsigned int rate); + void AudioCB(int16_t * buf, size_t samples_count); + void EventCB(ChiakiEvent *event); + bool InitVideo(int video_width, int video_height, int screen_width, int screen_height); + bool FreeVideo(); + bool InitJoystick(); + bool FreeJoystick(); + bool ReadUserKeyboard(char * buffer, size_t buffer_size); + bool MainLoop(ChiakiControllerState * state); +}; + +#endif //CHIAKI_IO_H + + diff --git a/switch/include/settings.h b/switch/include/settings.h new file mode 100644 index 0000000..abcbe46 --- /dev/null +++ b/switch/include/settings.h @@ -0,0 +1,105 @@ +/* + * This file is part of Chiaki. + * + * Chiaki is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Chiaki is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Chiaki. If not, see . + */ + +#ifndef CHIAKI_SETTINGS_H +#define CHIAKI_SETTINGS_H + +#include + +#include "host.h" +#include + +// mutual host and settings +class Host; + +class Settings +{ + private: + ChiakiLog * log; + const char * filename = "chiaki.conf"; + + std::map *hosts; + // global_settings from psedo INI file + ChiakiVideoResolutionPreset global_video_resolution = CHIAKI_VIDEO_RESOLUTION_PRESET_720p; + ChiakiVideoFPSPreset global_video_fps = CHIAKI_VIDEO_FPS_PRESET_60; + std::string global_psn_online_id = ""; + std::string global_psn_account_id = ""; + + typedef enum configurationitem + { + UNKNOWN, + HOST_NAME, + HOST_IP, + PSN_ONLINE_ID, + PSN_ACCOUNT_ID, + RP_KEY, + RP_KEY_TYPE, + RP_REGIST_KEY, + VIDEO_RESOLUTION, + VIDEO_FPS, + } ConfigurationItem; + + // dummy parser implementation + // the aim is not to have bulletproof parser + // the goal is to read/write inernal flat configuration file + const std::map re_map = { + { HOST_NAME, std::regex("^\\[\\s*(.+)\\s*\\]") }, + { HOST_IP, std::regex("^\\s*host_ip\\s*=\\s*\"?(\\d+\\.\\d+\\.\\d+\\.\\d+)\"?") }, + { PSN_ONLINE_ID, std::regex("^\\s*psn_online_id\\s*=\\s*\"?(\\w+)\"?") }, + { PSN_ACCOUNT_ID, std::regex("^\\s*psn_account_id\\s*=\\s*\"?([\\w/=+]+)\"?") }, + { RP_KEY, std::regex("^\\s*rp_key\\s*=\\s*\"?([\\w/=+]+)\"?") }, + { RP_KEY_TYPE, std::regex("^\\s*rp_key_type\\s*=\\s*\"?(\\d)\"?") }, + { RP_REGIST_KEY, std::regex("^\\s*rp_regist_key\\s*=\\s*\"?([\\w/=+]+)\"?") }, + { VIDEO_RESOLUTION, std::regex("^\\s*video_resolution\\s*=\\s*\"?(1080p|720p|540p|360p)\"?") }, + { VIDEO_FPS, std::regex("^\\s*video_fps\\s*=\\s*\"?(60|30)\"?") }, + }; + + ConfigurationItem ParseLine(std::string * line, std::string * value); + size_t GetB64encodeSize(size_t); + public: + Settings(ChiakiLog * log, std::map * hosts): log(log), hosts(hosts){}; + Host * GetOrCreateHost(std::string * host_name); + ChiakiLog* GetLogger(); + std::string GetPSNOnlineID(Host * host); + std::string GetPSNAccountID(Host * host); + void SetPSNOnlineID(Host * host, std::string psn_online_id); + void SetPSNAccountID(Host * host, std::string psn_account_id); + ChiakiVideoResolutionPreset GetVideoResolution(Host * host); + ChiakiVideoFPSPreset GetVideoFPS(Host * host); + std::string ResolutionPresetToString(ChiakiVideoResolutionPreset resolution); + std::string FPSPresetToString(ChiakiVideoFPSPreset fps); + ChiakiVideoResolutionPreset StringToResolutionPreset(std::string value); + ChiakiVideoFPSPreset StringToFPSPreset(std::string value); + int ResolutionPresetToInt(ChiakiVideoResolutionPreset resolution); + int FPSPresetToInt(ChiakiVideoFPSPreset fps); + void SetVideoResolution(Host * host, ChiakiVideoResolutionPreset value); + void SetVideoFPS(Host * host, ChiakiVideoFPSPreset value); + void SetVideoResolution(Host * host, std::string value); + void SetVideoFPS(Host * host, std::string value); + std::string GetHostIPAddr(Host * host); + std::string GetHostName(Host * host); + bool SetHostRPKeyType(Host * host, std::string value); + int GetHostRPKeyType(Host * host); + std::string GetHostRPKey(Host * host); + std::string GetHostRPRegistKey(Host * host); + bool SetHostRPKey(Host * host, std::string rp_key_b64); + bool SetHostRPRegistKey(Host * host, std::string rp_regist_key_b64); + void ParseFile(); + int WriteFile(); +}; + +#endif // CHIAKI_SETTINGS_H diff --git a/switch/res/add-24px.svg b/switch/res/add-24px.svg new file mode 120000 index 0000000..4a76ba4 --- /dev/null +++ b/switch/res/add-24px.svg @@ -0,0 +1 @@ +../../gui/res/add-24px.svg \ No newline at end of file diff --git a/switch/res/console.svg b/switch/res/console.svg new file mode 120000 index 0000000..a7f858c --- /dev/null +++ b/switch/res/console.svg @@ -0,0 +1 @@ +../../assets/console.svg \ No newline at end of file diff --git a/switch/res/discover-24px.svg b/switch/res/discover-24px.svg new file mode 120000 index 0000000..a7c3cc3 --- /dev/null +++ b/switch/res/discover-24px.svg @@ -0,0 +1 @@ +../../gui/res/discover-24px.svg \ No newline at end of file diff --git a/switch/res/discover-off-24px.svg b/switch/res/discover-off-24px.svg new file mode 120000 index 0000000..2c80caa --- /dev/null +++ b/switch/res/discover-off-24px.svg @@ -0,0 +1 @@ +../../gui/res/discover-off-24px.svg \ No newline at end of file diff --git a/switch/res/i18n/en-US b/switch/res/i18n/en-US new file mode 120000 index 0000000..dec3de9 --- /dev/null +++ b/switch/res/i18n/en-US @@ -0,0 +1 @@ +../../../third-party/borealis/resources/i18n/en-US \ No newline at end of file diff --git a/switch/res/icon.jpg b/switch/res/icon.jpg new file mode 100644 index 0000000000000000000000000000000000000000..cb515da077efce82a1d9aab5241a7cf15d30fd57 GIT binary patch literal 25090 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. + */ + +#ifdef __SWITCH__ +#include +#endif + +#include +#include +#include +#include +#include + +#include + +#define PING_MS 500 +#define HOSTS_MAX 16 +#define DROP_PINGS 3 + +static void Discovery(ChiakiDiscoveryHost * discovered_hosts, size_t hosts_count, void * user) +{ + DiscoveryManager * dm = (DiscoveryManager *) user; + for(size_t i=0; i < hosts_count; i++) + { + dm->DiscoveryCB(discovered_hosts+i); + } +} + + +DiscoveryManager::DiscoveryManager(Settings *settings) + : settings(settings), host_addr(nullptr), host_addr_len(0) +{ + this->log = this->settings->GetLogger(); +} + +DiscoveryManager::~DiscoveryManager() +{ + // join discovery thread + if(this->service_enable) + { + SetService(false); + } + + chiaki_discovery_fini(&this->discovery); +} + +void DiscoveryManager::SetService(bool enable) +{ + if(this->service_enable == enable) + { + return; + } + + this->service_enable = enable; + + if(enable) + { + ChiakiDiscoveryServiceOptions options; + options.ping_ms = PING_MS; + options.hosts_max = HOSTS_MAX; + options.host_drop_pings = DROP_PINGS; + options.cb = Discovery; + options.cb_user = this; + + sockaddr_in addr = {}; + addr.sin_family = AF_INET; + addr.sin_port = htons(CHIAKI_DISCOVERY_PORT); + addr.sin_addr.s_addr = GetIPv4BroadcastAddr(); + options.send_addr = reinterpret_cast(&addr); + options.send_addr_size = sizeof(addr); + + ChiakiErrorCode err = chiaki_discovery_service_init(&this->service, &options, log); + if(err != CHIAKI_ERR_SUCCESS) + { + this->service_enable = false; + CHIAKI_LOGE(this->log, "DiscoveryManager failed to init Discovery Service"); + return; + } + } + else + { + chiaki_discovery_service_fini(&this->service); + } +} + +uint32_t DiscoveryManager::GetIPv4BroadcastAddr() +{ +#ifdef __SWITCH__ + uint32_t current_addr, subnet_mask; + // init nintendo net interface service + Result rc = nifmInitialize(NifmServiceType_User); + if (R_SUCCEEDED(rc)) + { + // read current IP and netmask + rc = nifmGetCurrentIpConfigInfo( + ¤t_addr, &subnet_mask, + NULL, NULL, NULL); + nifmExit(); + } + else + { + CHIAKI_LOGE(this->log, "Failed to get nintendo nifmGetCurrentIpConfigInfo"); + return 1; + } + return current_addr | (~subnet_mask); +#else + return 0xffffffff; +#endif +} + +int DiscoveryManager::Send(struct sockaddr *host_addr, size_t host_addr_len) +{ + if(!host_addr) + { + CHIAKI_LOGE(log, "Null sockaddr"); + return 1; + } + ((struct sockaddr_in *)host_addr)->sin_port = htons(CHIAKI_DISCOVERY_PORT); + + ChiakiDiscoveryPacket packet; + memset(&packet, 0, sizeof(packet)); + packet.cmd = CHIAKI_DISCOVERY_CMD_SRCH; + + chiaki_discovery_send(&this->discovery, &packet, this->host_addr, this->host_addr_len); + return 0; +} + +int DiscoveryManager::Send(const char * discover_ip_dest) +{ + struct addrinfo * host_addrinfos; + int r = getaddrinfo(discover_ip_dest, NULL, NULL, &host_addrinfos); + if(r != 0) + { + CHIAKI_LOGE(log, "getaddrinfo failed"); + return 1; + } + + struct sockaddr * host_addr = nullptr; + socklen_t host_addr_len = 0; + + for(struct addrinfo *ai=host_addrinfos; ai; ai=ai->ai_next) + { + if(ai->ai_protocol != IPPROTO_UDP) + continue; + if(ai->ai_family != AF_INET) // TODO: IPv6 + continue; + + this->host_addr_len = ai->ai_addrlen; + this->host_addr = (struct sockaddr *)malloc(host_addr_len); + if(!this->host_addr) + break; + memcpy(this->host_addr, ai->ai_addr, this->host_addr_len); + } + + freeaddrinfo(host_addrinfos); + + if(!this->host_addr) + { + CHIAKI_LOGE(log, "Failed to get addr for hostname"); + return 1; + } + return DiscoveryManager::Send(this->host_addr, this->host_addr_len); +} + +int DiscoveryManager::Send() +{ + struct sockaddr_in addr; + addr.sin_family = AF_INET; + addr.sin_addr.s_addr = GetIPv4BroadcastAddr(); + addr.sin_port = htons(CHIAKI_DISCOVERY_PORT); + + this->host_addr_len = sizeof(sockaddr_in); + this->host_addr = (struct sockaddr *)malloc(host_addr_len); + memcpy(this->host_addr, &addr, this->host_addr_len); + + return DiscoveryManager::Send(this->host_addr, this->host_addr_len); +} + + +void DiscoveryManager::DiscoveryCB(ChiakiDiscoveryHost * discovered_host) +{ + // the user ptr is passed as + // chiaki_discovery_thread_start arg + + std::string key = discovered_host->host_name; + Host *host = this->settings->GetOrCreateHost(&key); + + CHIAKI_LOGI(this->log, "--"); + CHIAKI_LOGI(this->log, "Discovered Host:"); + CHIAKI_LOGI(this->log, "State: %s", chiaki_discovery_host_state_string(discovered_host->state)); + /* + host attr + uint32_t host_addr; + int system_version; + int device_discovery_protocol_version; + std::string host_name; + std::string host_type; + std::string host_id; + */ + host->state = discovered_host->state; + + // add host ptr to list + if(discovered_host->system_version) + { + // example: 07020001 + host->system_version = atoi(discovered_host->system_version); + CHIAKI_LOGI(this->log, "System Version: %s", discovered_host->system_version); + } + + if(discovered_host->device_discovery_protocol_version) + { + host->device_discovery_protocol_version = atoi(discovered_host->device_discovery_protocol_version); + CHIAKI_LOGI(this->log, "Device Discovery Protocol Version: %s", discovered_host->device_discovery_protocol_version); + } + + if(discovered_host->host_request_port) + CHIAKI_LOGI(this->log, "Request Port: %hu", (unsigned short)discovered_host->host_request_port); + + if(discovered_host->host_addr) + { + host->host_addr = discovered_host->host_addr; + CHIAKI_LOGI(this->log, "Host Addr: %s", discovered_host->host_addr); + } + + if(discovered_host->host_name) + { + host->host_name = discovered_host->host_name; + CHIAKI_LOGI(this->log, "Host Name: %s", discovered_host->host_name); + } + + if(discovered_host->host_type) + CHIAKI_LOGI(this->log, "Host Type: %s", discovered_host->host_type); + + if(discovered_host->host_id) + CHIAKI_LOGI(this->log, "Host ID: %s", discovered_host->host_id); + + if(discovered_host->running_app_titleid) + CHIAKI_LOGI(this->log, "Running App Title ID: %s", discovered_host->running_app_titleid); + + if(discovered_host->running_app_name) + CHIAKI_LOGI(this->log, "Running App Name: %s", discovered_host->running_app_name); + + CHIAKI_LOGI(this->log, "--"); +} + diff --git a/switch/src/gui.cpp b/switch/src/gui.cpp new file mode 100644 index 0000000..b3997b5 --- /dev/null +++ b/switch/src/gui.cpp @@ -0,0 +1,514 @@ +/* + * This file is part of Chiaki. + * + * Chiaki is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Chiaki is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Chiaki. If not, see . + */ + +#include +#include "gui.h" + +#define SCREEN_W 1280 +#define SCREEN_H 720 + +// TODO +using namespace brls::i18n::literals; // for _i18n + +#define DIALOG(dialog, r) \ + brls::Dialog* d_##dialog = new brls::Dialog(r); \ + brls::GenericEvent::Callback cb_##dialog = [d_##dialog](brls::View* view) \ + { \ + d_##dialog->close(); \ + }; \ + d_##dialog->addButton("Ok", cb_##dialog); \ + d_##dialog->setCancelable(false); \ + d_##dialog->open(); \ + brls::Logger::info("Dialog: {0}", r); + + +HostInterface::HostInterface(brls::List * hostList, IO * io, Host * host, Settings * settings) + : hostList(hostList), io(io), host(host), settings(settings) +{ + brls::ListItem* connect = new brls::ListItem("Connect"); + connect->getClickEvent()->subscribe(std::bind(&HostInterface::Connect, this, std::placeholders::_1)); + this->hostList->addView(connect); + + brls::ListItem* wakeup = new brls::ListItem("Wakeup"); + wakeup->getClickEvent()->subscribe(std::bind(&HostInterface::Wakeup, this, std::placeholders::_1)); + this->hostList->addView(wakeup); + + // message delimiter + brls::Label* info = new brls::Label(brls::LabelStyle::REGULAR, + "Host configuration", true); + this->hostList->addView(info); + + // push opengl chiaki stream + // when the host is connected + this->io->SetEventConnectedCallback(std::bind(&HostInterface::Stream, this)); + this->io->SetEventQuitCallback(std::bind(&HostInterface::CloseStream, this, std::placeholders::_1)); +} + +HostInterface::~HostInterface() +{ + Disconnect(); +} + +void HostInterface::Register(bool pin_incorrect) +{ + if(pin_incorrect) + { + DIALOG(srfpvyps, "Session Registration Failed, Please verify your PS4 settings"); + return; + } + + // the host is not registered yet + brls::Dialog* peprpc = new brls::Dialog("Please enter your PS4 registration PIN code"); + brls::GenericEvent::Callback cb_peprpc = [this, peprpc](brls::View* view) + { + bool pin_provided = false; + char pin_input[9] = {0}; + std::string error_message; + + // use callback to ensure that the message is showed on screen + // before the the ReadUserKeyboard + peprpc->close(); + + pin_provided = this->io->ReadUserKeyboard(pin_input, sizeof(pin_input)); + if(pin_provided) + { + // prevent users form messing with the gui + brls::Application::blockInputs(); + int ret = this->host->Register(pin_input); + if(ret != HOST_REGISTER_OK) + { + switch(ret) + { + // account not configured + case HOST_REGISTER_ERROR_SETTING_PSNACCOUNTID: + brls::Application::notify("No PSN Account ID provided"); + brls::Application::unblockInputs(); + break; + case HOST_REGISTER_ERROR_SETTING_PSNONLINEID: + brls::Application::notify("No PSN Online ID provided"); + brls::Application::unblockInputs(); + break; + } + } + } + }; + peprpc->addButton("Ok", cb_peprpc); + peprpc->setCancelable(false); + peprpc->open(); +} + +void HostInterface::Wakeup(brls::View * view) +{ + if(!this->host->rp_key_data) + { + // the host is not registered yet + DIALOG(prypf, "Please register your PS4 first"); + } + else + { + int r = host->Wakeup(); + if(r == 0) + { + brls::Application::notify("PS4 Wakeup packet sent"); + } + else + { + brls::Application::notify("PS4 Wakeup packet failed"); + } + } +} + +void HostInterface::Connect(brls::View * view) +{ + // check that all requirements are met + if(this->host->state != CHIAKI_DISCOVERY_HOST_STATE_READY) + { + // host in standby mode + DIALOG(ptoyp, "Please turn on your PS4"); + return; + } + + if(!this->host->rp_key_data) + { + // user must provide psn id for registration + std::string account_id = this->settings->GetPSNAccountID(this->host); + std::string online_id = this->settings->GetPSNOnlineID(this->host); + if(this->host->system_version >= 7000000 && account_id.length() <= 0) + { + // PS4 firmware > 7.0 + DIALOG(upaid, "Undefined PSN Account ID (Please configure a valid psn_account_id)"); + return; + } + else if( this->host->system_version < 7000000 && this->host->system_version > 0 && online_id.length() <= 0) + { + // use oline ID for ps4 < 7.0 + DIALOG(upoid, "Undefined PSN Online ID (Please configure a valid psn_online_id)"); + return; + } + + // add HostConnected function to regist_event_type_finished_success + auto event_type_finished_success_cb = [this]() + { + // save RP keys + this->settings->WriteFile(); + // FIXME: may raise a connection refused + // when the connection is triggered + // just after the register success + sleep(2); + ConnectSession(); + // decrement block input token number + brls::Application::unblockInputs(); + }; + this->host->SetRegistEventTypeFinishedSuccess(event_type_finished_success_cb); + + auto event_type_finished_failed_cb = [this]() + { + // unlock user inputs + brls::Application::unblockInputs(); + brls::Application::notify("Registration failed"); + }; + this->host->SetRegistEventTypeFinishedFailed(event_type_finished_failed_cb); + + this->Register(false); + } + else + { + // the host is already registered + // start session directly + ConnectSession(); + } +} + +void HostInterface::ConnectSession() +{ + // ignore all user inputs (avoid double connect) + // user inputs are restored with the CloseStream + brls::Application::blockInputs(); + + // connect host sesssion + this->host->InitSession(this->io); + this->host->StartSession(); +} + +void HostInterface::Disconnect() +{ + if(this->connected) + { + brls::Application::popView(); + this->host->StopSession(); + this->connected = false; + } + this->host->FiniSession(); +} + +bool HostInterface::Stream() +{ + this->connected = true; + // https://github.com/natinusala/borealis/issues/59 + // disable 60 fps limit + brls::Application::setMaximumFPS(0); + + // show FPS counter + // brls::Application::setDisplayFramerate(true); + + // push raw opengl stream over borealis + brls::Application::pushView(new PS4RemotePlay(this->io, this->host)); + return true; +} + +bool HostInterface::CloseStream(ChiakiQuitEvent * quit) +{ + // session QUIT call back + brls::Application::unblockInputs(); + + // restore 60 fps limit + brls::Application::setMaximumFPS(60); + + // brls::Application::setDisplayFramerate(false); + /* + DIALOG(sqrs, chiaki_quit_reason_string(quit->reason)); + */ + brls::Application::notify(chiaki_quit_reason_string(quit->reason)); + Disconnect(); + return false; +} + +MainApplication::MainApplication(std::map * hosts, + Settings * settings, DiscoveryManager * discoverymanager, + IO * io, ChiakiLog * log) + : hosts(hosts), settings(settings), discoverymanager(discoverymanager), + io(io), log(log) +{ +} + +MainApplication::~MainApplication() +{ + this->discoverymanager->SetService(false); + //this->io->FreeJoystick(); + this->io->FreeVideo(); +} + +bool MainApplication::Load() +{ + this->discoverymanager->SetService(true); + // Init the app + brls::Logger::setLogLevel(brls::LogLevel::DEBUG); + + brls::i18n::loadTranslations(); + if (!brls::Application::init("Chiaki Remote play")) + { + brls::Logger::error("Unable to init Borealis application"); + return false; + } + + // init chiaki gl after borealis + // let borealis manage the main screen/window + + if(!io->InitVideo(0, 0, SCREEN_W, SCREEN_H)) + { + brls::Logger::error("Failed to initiate Video"); + } + + brls::Logger::info("Load sdl joysticks"); + if(!io->InitJoystick()) + { + brls::Logger::error("Faled to initiate Joysticks"); + } + + // Create a view + this->rootFrame = new brls::TabFrame(); + this->rootFrame->setTitle("Chiaki: Open Source PS4 Remote Play Client"); + this->rootFrame->setIcon(BOREALIS_ASSET("icon.jpg")); + + brls::List* config = new brls::List(); + BuildConfigurationMenu(config); + + this->rootFrame->addTab("Configuration", config); + // ---------------- + this->rootFrame->addSeparator(); + + // Add the root view to the stack + brls::Application::pushView(this->rootFrame); + while(brls::Application::mainLoop()) + { + for(auto it = this->hosts->begin(); it != this->hosts->end(); it++) + { + if(this->host_menuitems.find(&it->second) == this->host_menuitems.end()) + { + brls::List* new_host = new brls::List(); + this->host_menuitems[&it->second] = new_host; + // create host if udefined + HostInterface host_menu = HostInterface(new_host, this->io, &it->second, this->settings); + BuildConfigurationMenu(new_host, &it->second); + this->rootFrame->addTab(it->second.host_name.c_str(), new_host); + } + } + } + return true; +} + +bool MainApplication::BuildConfigurationMenu(brls::List * ls, Host * host) +{ + std::string psn_online_id_string = this->settings->GetPSNOnlineID(host); + brls::ListItem* psn_online_id = new brls::ListItem("PSN Online ID"); + psn_online_id->setValue(psn_online_id_string.c_str()); + auto psn_online_id_cb = [this, host, psn_online_id](brls::View * view) + { + char online_id[256] = {0}; + bool input = this->io->ReadUserKeyboard(online_id, sizeof(online_id)); + if(input) + { + // update gui + psn_online_id->setValue(online_id); + // push in setting + this->settings->SetPSNOnlineID(host, online_id); + // write on disk + this->settings->WriteFile(); + } + }; + psn_online_id->getClickEvent()->subscribe(psn_online_id_cb); + ls->addView(psn_online_id); + + std::string psn_account_id_string = this->settings->GetPSNAccountID(host); + brls::ListItem* psn_account_id = new brls::ListItem("PSN Account ID", "v7.0 and greater"); + psn_account_id->setValue(psn_account_id_string.c_str()); + auto psn_account_id_cb = [this, host, psn_account_id](brls::View * view) + { + char account_id[CHIAKI_PSN_ACCOUNT_ID_SIZE * 2] = {0}; + bool input = this->io->ReadUserKeyboard(account_id, sizeof(account_id)); + if(input) + { + // update gui + psn_account_id->setValue(account_id); + // push in setting + this->settings->SetPSNAccountID(host, account_id); + // write on disk + this->settings->WriteFile(); + } + }; + psn_account_id->getClickEvent()->subscribe(psn_account_id_cb); + ls->addView(psn_account_id); + + int value; + ChiakiVideoResolutionPreset resolution_preset = this->settings->GetVideoResolution(host); + switch(resolution_preset) + { + case CHIAKI_VIDEO_RESOLUTION_PRESET_720p: + value = 0; + break; + case CHIAKI_VIDEO_RESOLUTION_PRESET_540p: + value = 1; + break; + case CHIAKI_VIDEO_RESOLUTION_PRESET_360p: + value = 2; + break; + } + + brls::SelectListItem* resolution = new brls::SelectListItem( + "Resolution", { "720p", "540p", "360p" }, value); + + auto resolution_cb = [this, host](int result) + { + ChiakiVideoResolutionPreset value = CHIAKI_VIDEO_RESOLUTION_PRESET_720p; + switch(result) + { + case 0: + value = CHIAKI_VIDEO_RESOLUTION_PRESET_720p; + break; + case 1: + value = CHIAKI_VIDEO_RESOLUTION_PRESET_540p; + break; + case 2: + value = CHIAKI_VIDEO_RESOLUTION_PRESET_360p; + break; + } + this->settings->SetVideoResolution(host, value); + this->settings->WriteFile(); + }; + resolution->getValueSelectedEvent()->subscribe(resolution_cb); + ls->addView(resolution); + + ChiakiVideoFPSPreset fps_preset = this->settings->GetVideoFPS(host); + switch(fps_preset) + { + case CHIAKI_VIDEO_FPS_PRESET_60: + value = 0; + break; + case CHIAKI_VIDEO_FPS_PRESET_30: + value = 1; + break; + } + + brls::SelectListItem* fps = new brls::SelectListItem( + "FPS", { "60", "30"}, value); + + auto fps_cb = [this, host](int result) + { + ChiakiVideoFPSPreset value = CHIAKI_VIDEO_FPS_PRESET_60; + switch(result) + { + case 0: + value = CHIAKI_VIDEO_FPS_PRESET_60; + break; + case 1: + value = CHIAKI_VIDEO_FPS_PRESET_30; + break; + } + this->settings->SetVideoFPS(host, value); + this->settings->WriteFile(); + }; + + fps->getValueSelectedEvent()->subscribe(fps_cb); + ls->addView(fps); + + if(host != nullptr) + { + // message delimiter + brls::Label* info = new brls::Label(brls::LabelStyle::REGULAR, + "Host information", true); + ls->addView(info); + + std::string host_name_string = this->settings->GetHostName(host); + brls::ListItem* host_name = new brls::ListItem("PS4 Hostname"); + host_name->setValue(host_name_string.c_str()); + ls->addView(host_name); + + std::string host_ipaddr_string = settings->GetHostIPAddr(host); + brls::ListItem* host_ipaddr = new brls::ListItem("PS4 IP Address"); + host_ipaddr->setValue(host_ipaddr_string.c_str()); + ls->addView(host_ipaddr); + + std::string host_rp_regist_key_string = settings->GetHostRPRegistKey(host); + brls::ListItem* host_rp_regist_key = new brls::ListItem("RP Register Key"); + host_rp_regist_key->setValue(host_rp_regist_key_string.c_str()); + ls->addView(host_rp_regist_key); + + std::string host_rp_key_string = settings->GetHostRPKey(host); + brls::ListItem* host_rp_key = new brls::ListItem("RP Key"); + host_rp_key->setValue(host_rp_key_string.c_str()); + ls->addView(host_rp_key); + + std::string host_rp_key_type_string = std::to_string(settings->GetHostRPKeyType(host)); + brls::ListItem* host_rp_key_type = new brls::ListItem("RP Key type"); + host_rp_key_type->setValue(host_rp_key_type_string.c_str()); + ls->addView(host_rp_key_type); + + } + + return true; +} + +PS4RemotePlay::PS4RemotePlay(IO * io, Host * host) + : io(io), host(host) +{ + // store joycon/touchpad keys + for(int x=0; x < CHIAKI_CONTROLLER_TOUCHES_MAX; x++) + // start touchpad as "untouched" + this->state.touches[x].id = -1; + + // this->base_time=glfwGetTime(); +} + +void PS4RemotePlay::draw(NVGcontext* vg, int x, int y, unsigned width, unsigned height, brls::Style* style, brls::FrameContext* ctx) +{ + this->io->MainLoop(&this->state); + this->host->SendFeedbackState(&state); + + // FPS calculation + // this->frame_counter += 1; + // double frame_time = glfwGetTime(); + // if((frame_time - base_time) >= 1.0) + // { + // base_time += 1; + // //printf("FPS: %d\n", this->frame_counter); + // this->fps = this->frame_counter; + // this->frame_counter = 0; + // } + // nvgBeginPath(vg); + // nvgFillColor(vg, nvgRGBA(255,192,0,255)); + // nvgFontFaceId(vg, ctx->fontStash->regular); + // nvgFontSize(vg, style->Label.smallFontSize); + // nvgTextAlign(vg, NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE); + // char fps_str[9] = {0}; + // sprintf(fps_str, "FPS: %000d", this->fps); + // nvgText(vg, 5,10, fps_str, NULL); +} + +PS4RemotePlay::~PS4RemotePlay() +{ +} + diff --git a/switch/src/host.cpp b/switch/src/host.cpp new file mode 100644 index 0000000..2b3dcf3 --- /dev/null +++ b/switch/src/host.cpp @@ -0,0 +1,287 @@ +/* + * This file is part of Chiaki. + * + * Chiaki is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Chiaki is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Chiaki. If not, see . + */ + + +#include + +#include + +#include "io.h" +#include "host.h" + + +static void InitAudioCB(unsigned int channels, unsigned int rate, void * user) +{ + IO * io = (IO *) user; + io->InitAudioCB(channels, rate); +} + +static bool VideoCB(uint8_t * buf, size_t buf_size, void * user) +{ + IO * io = (IO *) user; + return io->VideoCB(buf, buf_size); +} + +static void AudioCB(int16_t * buf, size_t samples_count, void * user) +{ + IO * io = (IO *) user; + io->AudioCB(buf, samples_count); +} + +static void EventCB(ChiakiEvent * event, void * user) +{ + IO * io = (IO *) user; + io->EventCB(event); +} + +static void RegistEventCB(ChiakiRegistEvent * event, void * user) +{ + Host * host = (Host *) user; + host->RegistCB(event); +} + +Host::Host(ChiakiLog * log, Settings * settings, std::string host_name) + : log(log), settings(settings), host_name(host_name) +{ +} + +Host::~Host() +{ +} + +int Host::Wakeup() +{ + if(strlen(this->rp_regist_key) > 8) + { + CHIAKI_LOGE(this->log, "Given registkey is too long"); + return 1; + } + else if (strlen(this->rp_regist_key) <=0) + { + CHIAKI_LOGE(this->log, "Given registkey is not defined"); + return 2; + } + + uint64_t credential = (uint64_t)strtoull(this->rp_regist_key, NULL, 16); + ChiakiErrorCode ret = chiaki_discovery_wakeup(this->log, NULL, host_addr.c_str(), credential); + if(ret == CHIAKI_ERR_SUCCESS) + { + //FIXME + } + return ret; +} + +int Host::Register(std::string pin) +{ + // use pin and accont_id to negociate secrets for session + // + // convert psn_account_id into uint8_t[CHIAKI_PSN_ACCOUNT_ID_SIZE] + // CHIAKI_PSN_ACCOUNT_ID_SIZE == 8 + std::string account_id = this->settings->GetPSNAccountID(this); + std::string online_id = this->settings->GetPSNOnlineID(this); + size_t account_id_size = sizeof(uint8_t[CHIAKI_PSN_ACCOUNT_ID_SIZE]); + + // PS4 firmware > 7.0 + if(this->system_version >= 7000000) + { + // use AccountID for ps4 > 7.0 + if(account_id.length() > 0) + { + chiaki_base64_decode(account_id.c_str(), account_id.length(), + regist_info.psn_account_id, &(account_id_size)); + regist_info.psn_online_id = nullptr; + } + else + { + CHIAKI_LOGE(this->log, "Undefined PSN Account ID (Please configure a valid psn_account_id)"); + return HOST_REGISTER_ERROR_SETTING_PSNACCOUNTID; + } + if(this->system_version >= 8000000) + regist_info.target = CHIAKI_TARGET_PS4_10; + else + regist_info.target = CHIAKI_TARGET_PS4_9; + } + else if( this->system_version < 7000000 && this->system_version > 0) + { + // use oline ID for ps4 < 7.0 + if(online_id.length() > 0) + { + regist_info.psn_online_id = this->psn_online_id.c_str(); + // regist_info.psn_account_id = {0}; + } + else + { + CHIAKI_LOGE(this->log, "Undefined PSN Online ID (Please configure a valid psn_online_id)"); + return HOST_REGISTER_ERROR_SETTING_PSNONLINEID; + } + regist_info.target = CHIAKI_TARGET_PS4_8; + } + else + { + CHIAKI_LOGE(this->log, "Undefined PS4 system version (please run discover first)"); + } + + this->regist_info.pin = atoi(pin.c_str()); + this->regist_info.host = this->host_addr.c_str(); + this->regist_info.broadcast = false; + CHIAKI_LOGI(this->log, "Registering to host `%s` `%s` with PSN AccountID `%s` pin `%s`", + this->host_name.c_str(), this->host_addr.c_str(), psn_account_id.c_str(), pin.c_str()); + chiaki_regist_start(&this->regist, this->log, &this->regist_info, RegistEventCB, this); + return HOST_REGISTER_OK; +} + +int Host::InitSession(IO * user) +{ + chiaki_connect_video_profile_preset(&(this->video_profile), + this->video_resolution, this->video_fps); + // Build chiaki ps4 stream session + chiaki_opus_decoder_init(&(this->opus_decoder), this->log); + ChiakiAudioSink audio_sink; + ChiakiConnectInfo chiaki_connect_info; + + chiaki_connect_info.host = this->host_addr.c_str(); + chiaki_connect_info.video_profile = this->video_profile; + memcpy(chiaki_connect_info.regist_key, this->rp_regist_key, sizeof(chiaki_connect_info.regist_key)); + memcpy(chiaki_connect_info.morning, this->rp_key, sizeof(chiaki_connect_info.morning)); + // set keybord state to 0 + memset(&(this->keyboard_state), 0, sizeof(keyboard_state)); + + ChiakiErrorCode err = chiaki_session_init(&(this->session), &chiaki_connect_info, this->log); + if(err != CHIAKI_ERR_SUCCESS) + throw Exception(chiaki_error_string(err)); + this->session_init = true; + // audio setting_cb and frame_cb + chiaki_opus_decoder_set_cb(&this->opus_decoder, InitAudioCB, AudioCB, user); + chiaki_opus_decoder_get_sink(&this->opus_decoder, &audio_sink); + chiaki_session_set_audio_sink(&(this->session), &audio_sink); + chiaki_session_set_video_sample_cb(&(this->session), VideoCB, user); + chiaki_session_set_event_cb(&(this->session), EventCB, user); + return 0; +} + +int Host::FiniSession() +{ + if(this->session_init) + { + chiaki_session_join(&this->session); + chiaki_session_fini(&this->session); + chiaki_opus_decoder_fini(&this->opus_decoder); + } + return 0; +} + +void Host::StopSession() +{ + chiaki_session_stop(&this->session); +} + +void Host::StartSession() +{ + ChiakiErrorCode err = chiaki_session_start(&this->session); + if(err != CHIAKI_ERR_SUCCESS) + { + chiaki_session_fini(&this->session); + throw Exception("Chiaki Session Start failed"); + } +} + +void Host::SendFeedbackState(ChiakiControllerState * state) +{ + // send controller/joystick key + chiaki_session_set_controller_state(&this->session, state); +} + +void Host::RegistCB(ChiakiRegistEvent * event) +{ + // Chiaki callback fuction + // fuction called by lib chiaki regist + // durring client pin code registration + // + // read data from lib and record secrets into Host object + + this->registered = false; + switch(event->type) + { + case CHIAKI_REGIST_EVENT_TYPE_FINISHED_CANCELED: + CHIAKI_LOGI(this->log, "Register event CHIAKI_REGIST_EVENT_TYPE_FINISHED_CANCELED"); + if(this->chiaki_regist_event_type_finished_canceled != nullptr) + { + this->chiaki_regist_event_type_finished_canceled(); + } + break; + case CHIAKI_REGIST_EVENT_TYPE_FINISHED_FAILED: + CHIAKI_LOGI(this->log, "Register event CHIAKI_REGIST_EVENT_TYPE_FINISHED_FAILED"); + if(this->chiaki_regist_event_type_finished_failed != nullptr) + { + this->chiaki_regist_event_type_finished_failed(); + } + break; + case CHIAKI_REGIST_EVENT_TYPE_FINISHED_SUCCESS: + { + ChiakiRegisteredHost *r_host = event->registered_host; + CHIAKI_LOGI(this->log, "Register event CHIAKI_REGIST_EVENT_TYPE_FINISHED_SUCCESS"); + // copy values form ChiakiRegisteredHost object + this->ap_ssid = r_host->ap_ssid; + this->ap_key = r_host->ap_key; + this->ap_name = r_host->ap_name; + memcpy( &(this->ps4_mac), &(r_host->ps4_mac), sizeof(this->ps4_mac) ); + this->ps4_nickname = r_host->ps4_nickname; + memcpy( &(this->rp_regist_key), &(r_host->rp_regist_key), sizeof(this->rp_regist_key) ); + this->rp_key_type = r_host->rp_key_type; + memcpy( &(this->rp_key), &(r_host->rp_key), sizeof(this->rp_key) ); + // mark host as registered + this->registered = true; + this->rp_key_data = true; + CHIAKI_LOGI(this->log, "Register Success %s", this->host_name.c_str()); + + if(this->chiaki_regist_event_type_finished_success != nullptr) + this->chiaki_regist_event_type_finished_success(); + + break; + } + } + // close registration socket + chiaki_regist_stop(&this->regist); + chiaki_regist_fini(&this->regist); +} + +bool Host::GetVideoResolution(int * ret_width, int * ret_height) +{ + switch(this->video_resolution) + { + case CHIAKI_VIDEO_RESOLUTION_PRESET_360p: + *ret_width = 640; + *ret_height = 360; + break; + case CHIAKI_VIDEO_RESOLUTION_PRESET_540p: + *ret_width = 950; + *ret_height = 540; + break; + case CHIAKI_VIDEO_RESOLUTION_PRESET_720p: + *ret_width = 1280; + *ret_height = 720; + break; + case CHIAKI_VIDEO_RESOLUTION_PRESET_1080p: + *ret_width = 1920; + *ret_height = 1080; + break; + default: + return false; + } + return true; +} + diff --git a/switch/src/io.cpp b/switch/src/io.cpp new file mode 100644 index 0000000..c5e12aa --- /dev/null +++ b/switch/src/io.cpp @@ -0,0 +1,797 @@ +/* + * This file is part of Chiaki. + * + * Chiaki is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Chiaki is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Chiaki. If not, see . + */ + +#ifdef __SWITCH__ +#include +#else +#include +#endif + +#include "io.h" + +// https://github.com/matlo/GIMX/blob/3af491c3b6a89c6a76c9831f1f022a1b73a00752/shared/gimxcontroller/include/ds4.h#L112 +#define DS4_TRACKPAD_MAX_X 1919 +#define DS4_TRACKPAD_MAX_Y 919 +#define SWITCH_TOUCHSCREEN_MAX_X 1280 +#define SWITCH_TOUCHSCREEN_MAX_Y 720 + +// source: +// https://github.com/thestr4ng3r/chiaki/blob/master/gui/src/avopenglwidget.cpp +// +// examples : +// https://www.roxlu.com/2014/039/decoding-h264-and-yuv420p-playback +// https://gist.github.com/roxlu/9329339 + +// use OpenGl to decode YUV +// the aim is to spare CPU load on nintendo switch + +static const char* shader_vert_glsl = R"glsl( +#version 150 core +in vec2 pos_attr; +out vec2 uv_var; +void main() +{ + uv_var = pos_attr; + gl_Position = vec4(pos_attr * vec2(2.0, -2.0) + vec2(-1.0, 1.0), 0.0, 1.0); +} +)glsl"; + +static const char *yuv420p_shader_frag_glsl = R"glsl( +#version 150 core +uniform sampler2D plane1; // Y +uniform sampler2D plane2; // U +uniform sampler2D plane3; // V +in vec2 uv_var; +out vec4 out_color; +void main() +{ + vec3 yuv = vec3( + (texture(plane1, uv_var).r - (16.0 / 255.0)) / ((235.0 - 16.0) / 255.0), + (texture(plane2, uv_var).r - (16.0 / 255.0)) / ((240.0 - 16.0) / 255.0) - 0.5, + (texture(plane3, uv_var).r - (16.0 / 255.0)) / ((240.0 - 16.0) / 255.0) - 0.5); + vec3 rgb = mat3( + 1.0, 1.0, 1.0, + 0.0, -0.21482, 2.12798, + 1.28033, -0.38059, 0.0) * yuv; + out_color = vec4(rgb, 1.0); +} +)glsl"; + +static const float vert_pos[] = { + 0.0f, 0.0f, + 0.0f, 1.0f, + 1.0f, 0.0f, + 1.0f, 1.0f +}; + +IO::IO(ChiakiLog * log) + : log(log) +{ +} + +IO::~IO() +{ + //FreeJoystick(); + if(this->sdl_audio_device_id <= 0) + { + SDL_CloseAudioDevice(this->sdl_audio_device_id); + } + FreeVideo(); +} + +void IO::SetMesaConfig() +{ + //TRACE("%s", "Mesaconfig"); + //setenv("MESA_GL_VERSION_OVERRIDE", "3.3", 1); + //setenv("MESA_GLSL_VERSION_OVERRIDE", "330", 1); + // Uncomment below to disable error checking and save CPU time (useful for production): + //setenv("MESA_NO_ERROR", "1", 1); +#ifdef DEBUG_OPENGL + // Uncomment below to enable Mesa logging: + setenv("EGL_LOG_LEVEL", "debug", 1); + setenv("MESA_VERBOSE", "all", 1); + setenv("NOUVEAU_MESA_DEBUG", "1", 1); + + // Uncomment below to enable shader debugging in Nouveau: + //setenv("NV50_PROG_OPTIMIZE", "0", 1); + setenv("NV50_PROG_DEBUG", "1", 1); + //setenv("NV50_PROG_CHIPSET", "0x120", 1); +#endif +} + +#ifdef DEBUG_OPENGL +#define D(x){ (x); CheckGLError(__func__, __FILE__, __LINE__); } +void IO::CheckGLError(const char* func, const char* file, int line) +{ + GLenum err; + while( (err = glGetError()) != GL_NO_ERROR ) + { + CHIAKI_LOGE(this->log, "glGetError: %x function: %s from %s line %d", err, func, file, line); + //GL_INVALID_VALUE, 0x0501 + // Given when a value parameter is not a legal value for that function. T + // his is only given for local problems; + // if the spec allows the value in certain circumstances, + // where other parameters or state dictate those circumstances, + // then GL_INVALID_OPERATION is the result instead. + } +} + +#define DS(x){ DumpShaderError(x, __func__, __FILE__, __LINE__); } +void IO::DumpShaderError(GLuint shader, const char* func, const char* file, int line) +{ + GLchar str[512+1]; + GLsizei len = 0; + glGetShaderInfoLog(shader, 512, &len, str); + if (len > 512) len = 512; + str[len] = '\0'; + CHIAKI_LOGE(this->log, "glGetShaderInfoLog: %s function: %s from %s line %d", str, func, file, line); +} + +#define DP(x){ DumpProgramError(x, __func__, __FILE__, __LINE__); } +void IO::DumpProgramError(GLuint prog, const char* func, const char* file, int line) +{ + GLchar str[512+1]; + GLsizei len = 0; + glGetProgramInfoLog(prog, 512, &len, str); + if (len > 512) len = 512; + str[len] = '\0'; + CHIAKI_LOGE(this->log, "glGetProgramInfoLog: %s function: %s from %s line %d", str, func, file, line); +} + +#else +// do nothing +#define D(x){ (x); } +#define DS(x){ } +#define DP(x){ } +#endif + + +bool IO::VideoCB(uint8_t * buf, size_t buf_size) +{ + // callback function to decode video buffer + + AVPacket packet; + av_init_packet(&packet); + packet.data = buf; + packet.size = buf_size; + AVFrame * frame = av_frame_alloc(); + if(!frame) + { + CHIAKI_LOGE(this->log, "UpdateFrame Failed to alloc AVFrame"); + av_packet_unref(&packet); + return false; + } + +send_packet: + // Push + int r = avcodec_send_packet(this->codec_context, &packet); + if(r != 0) + { + if(r == AVERROR(EAGAIN)) + { + CHIAKI_LOGE(this->log, "AVCodec internal buffer is full removing frames before pushing"); + r = avcodec_receive_frame(this->codec_context, frame); + // send decoded frame for sdl texture update + if(r != 0) + { + CHIAKI_LOGE(this->log, "Failed to pull frame"); + av_frame_free(&frame); + av_packet_unref(&packet); + return false; + } + goto send_packet; + } + else + { + char errbuf[128]; + av_make_error_string(errbuf, sizeof(errbuf), r); + CHIAKI_LOGE(this->log, "Failed to push frame: %s", errbuf); + av_frame_free(&frame); + av_packet_unref(&packet); + return false; + } + } + + this->mtx.lock(); + // Pull + r = avcodec_receive_frame(this->codec_context, this->frame); + this->mtx.unlock(); + + if(r != 0) + CHIAKI_LOGE(this->log, "Failed to pull frame"); + + av_frame_free(&frame); + av_packet_unref(&packet); + return true; +} + + +void IO::InitAudioCB(unsigned int channels, unsigned int rate) +{ + SDL_AudioSpec want, have, test; + SDL_memset(&want, 0, sizeof(want)); + + //source + //[I] Audio Header: + //[I] channels = 2 + //[I] bits = 16 + //[I] rate = 48000 + //[I] frame size = 480 + //[I] unknown = 1 + want.freq = rate; + want.format = AUDIO_S16SYS; + // 2 == stereo + want.channels = channels; + want.samples = 1024; + want.callback = NULL; + + if(this->sdl_audio_device_id <= 0) + { + // the chiaki session might be called many times + // open the audio device only once + this->sdl_audio_device_id = SDL_OpenAudioDevice(NULL, 0, &want, NULL, 0); + } + + if(this->sdl_audio_device_id <= 0) + { + CHIAKI_LOGE(this->log, "SDL_OpenAudioDevice failed: %s\n", SDL_GetError()); + } + else + { + SDL_PauseAudioDevice(this->sdl_audio_device_id, 0); + } +} + +void IO::AudioCB(int16_t * buf, size_t samples_count) +{ + //int az = SDL_GetQueuedAudioSize(host->audio_device_id); + // len the number of bytes (not samples!) to which (data) points + int success = SDL_QueueAudio(this->sdl_audio_device_id, buf, sizeof(int16_t)*samples_count*2); + if(success != 0) + CHIAKI_LOGE(this->log, "SDL_QueueAudio failed: %s\n", SDL_GetError()); +} + +bool IO::InitVideo(int video_width, int video_height, int screen_width, int screen_height) +{ + CHIAKI_LOGV(this->log, "load InitVideo"); + this->video_width = video_width; + this->video_height = video_height; + + this->screen_width = screen_width; + this->screen_height = screen_height; + this->frame = av_frame_alloc(); + + if(!InitAVCodec()) + { + throw Exception("Failed to initiate libav codec"); + } + + if(!InitOpenGl()) + { + throw Exception("Failed to initiate OpenGl"); + } + return true; +} + +void IO::EventCB(ChiakiEvent *event) +{ + switch(event->type) + { + case CHIAKI_EVENT_CONNECTED: + CHIAKI_LOGI(this->log, "EventCB CHIAKI_EVENT_CONNECTED"); + if(this->chiaki_event_connected_cb != nullptr) + this->quit = !this->chiaki_event_connected_cb(); + else + this->quit = false; + break; + case CHIAKI_EVENT_LOGIN_PIN_REQUEST: + CHIAKI_LOGI(this->log, "EventCB CHIAKI_EVENT_LOGIN_PIN_REQUEST"); + if(this->chiaki_even_login_pin_request_cb != nullptr) + this->quit = !this->chiaki_even_login_pin_request_cb(event->login_pin_request.pin_incorrect); + break; + case CHIAKI_EVENT_QUIT: + CHIAKI_LOGI(this->log, "EventCB CHIAKI_EVENT_QUIT"); + if(this->chiaki_event_quit_cb != nullptr) + this->quit = !this->chiaki_event_quit_cb(&event->quit); + else + this->quit = true; + break; + } +} + + +bool IO::FreeVideo() +{ + bool ret = true; + + if(this->frame) + av_frame_free(&this->frame); + + // avcodec_alloc_context3(codec); + if(this->codec_context) + { + avcodec_close(this->codec_context); + avcodec_free_context(&this->codec_context); + } + + return ret; +} + +bool IO::ReadUserKeyboard(char *buffer, size_t buffer_size) +{ +#ifdef CHIAKI_SWITCH_ENABLE_LINUX + // use cin to get user input from linux + std::cin.getline(buffer, buffer_size); + CHIAKI_LOGI(this->log, "Got user input: %s\n", buffer); +#else + // https://kvadevack.se/post/nintendo-switch-virtual-keyboard/ + SwkbdConfig kbd; + Result rc = swkbdCreate(&kbd, 0); + + if (R_SUCCEEDED(rc)) + { + swkbdConfigMakePresetDefault(&kbd); + rc = swkbdShow(&kbd, buffer, buffer_size); + + if (R_SUCCEEDED(rc)) + { + CHIAKI_LOGI(this->log, "Got user input: %s\n", buffer); + } + else + { + CHIAKI_LOGE(this->log, "swkbdShow() error: %u\n", rc); + return false; + } + swkbdClose(&kbd); + } + else + { + CHIAKI_LOGE(this->log, "swkbdCreate() error: %u\n", rc); + return false; + } +#endif + return true; +} + +bool IO::ReadGameTouchScreen(ChiakiControllerState *state) +{ +#ifdef __SWITCH__ + hidScanInput(); + int touch_count = hidTouchCount(); + bool ret = false; + if(!touch_count) + { + for(int i=0; i < CHIAKI_CONTROLLER_TOUCHES_MAX; i++) + { + if(state->touches[i].id != -1) + { + state->touches[i].x = 0; + state->touches[i].y = 0; + state->touches[i].id = -1; + state->buttons &= ~CHIAKI_CONTROLLER_BUTTON_TOUCHPAD; // touchscreen release + // the state changed + ret = true; + } + } + return ret; + } + + touchPosition touch; + for(int i=0; i < touch_count && i < CHIAKI_CONTROLLER_TOUCHES_MAX; i++) + { + hidTouchRead(&touch, i); + + // 1280×720 px (16:9) + // ps4 controller aspect ratio looks closer to 29:10 + uint16_t x = touch.px * (DS4_TRACKPAD_MAX_X / SWITCH_TOUCHSCREEN_MAX_X); + uint16_t y = touch.py * (DS4_TRACKPAD_MAX_Y / SWITCH_TOUCHSCREEN_MAX_Y); + + // use nintendo switch border's 5% to + if(x <= (SWITCH_TOUCHSCREEN_MAX_X * 0.05) || x >= (SWITCH_TOUCHSCREEN_MAX_X * 0.95) + || y <= (SWITCH_TOUCHSCREEN_MAX_Y * 0.05) || y >= (SWITCH_TOUCHSCREEN_MAX_Y * 0.95)) + { + state->buttons |= CHIAKI_CONTROLLER_BUTTON_TOUCHPAD; // touchscreen + // printf("CHIAKI_CONTROLLER_BUTTON_TOUCHPAD\n"); + } + else + { + state->buttons &= ~CHIAKI_CONTROLLER_BUTTON_TOUCHPAD; // touchscreen release + } + + state->touches[i].x = x; + state->touches[i].y = y; + state->touches[i].id = i; + // printf("[point_id=%d] px=%03d, py=%03d, dx=%03d, dy=%03d, angle=%03d\n", + // i, touch.px, touch.py, touch.dx, touch.dy, touch.angle); + ret = true; + } + return ret; +#else + return false; +#endif +} + +bool IO::ReadGameKeys(SDL_Event *event, ChiakiControllerState *state) +{ + // return true if an event changed (gamepad input) + + // TODO + // share vs PS button + // Gyro ? + // rumble ? + bool ret = true; + switch(event->type) + { + case SDL_JOYAXISMOTION: + if(event->jaxis.which == 0) + { + // left joystick + if(event->jaxis.axis == 0) + // Left-right movement + state->left_x = event->jaxis.value; + else if(event->jaxis.axis == 1) + // Up-Down movement + state->left_y = event->jaxis.value; + else if(event->jaxis.axis == 2) + // Left-right movement + state->right_x = event->jaxis.value; + else if(event->jaxis.axis == 3) + // Up-Down movement + state->right_y = event->jaxis.value; + else + ret = false; + } + else if (event->jaxis.which == 1) + { + // right joystick + if(event->jaxis.axis == 0) + // Left-right movement + state->right_x = event->jaxis.value; + else if(event->jaxis.axis == 1) + // Up-Down movement + state->right_y = event->jaxis.value; + else + ret = false; + } + else + ret = false; + break; + case SDL_JOYBUTTONDOWN: + // printf("Joystick %d button %d DOWN\n", + // event->jbutton.which, event->jbutton.button); + switch(event->jbutton.button) + { + case 0: state->buttons |= CHIAKI_CONTROLLER_BUTTON_MOON; break; // KEY_A + case 1: state->buttons |= CHIAKI_CONTROLLER_BUTTON_CROSS; break; // KEY_B + case 2: state->buttons |= CHIAKI_CONTROLLER_BUTTON_PYRAMID; break; // KEY_X + case 3: state->buttons |= CHIAKI_CONTROLLER_BUTTON_BOX; break; // KEY_Y + case 12: state->buttons |= CHIAKI_CONTROLLER_BUTTON_DPAD_LEFT; break; // KEY_DLEFT + case 14: state->buttons |= CHIAKI_CONTROLLER_BUTTON_DPAD_RIGHT; break; // KEY_DRIGHT + case 13: state->buttons |= CHIAKI_CONTROLLER_BUTTON_DPAD_UP; break; // KEY_DUP + case 15: state->buttons |= CHIAKI_CONTROLLER_BUTTON_DPAD_DOWN; break; // KEY_DDOWN + case 6: state->buttons |= CHIAKI_CONTROLLER_BUTTON_L1; break; // KEY_L + case 7: state->buttons |= CHIAKI_CONTROLLER_BUTTON_R1; break; // KEY_R + case 8: state->l2_state = 0xff; break; // KEY_ZL + case 9: state->r2_state = 0xff; break; // KEY_ZR + case 4: state->buttons |= CHIAKI_CONTROLLER_BUTTON_L3; break; // KEY_LSTICK + case 5: state->buttons |= CHIAKI_CONTROLLER_BUTTON_R3; break; // KEY_RSTICK + case 10: state->buttons |= CHIAKI_CONTROLLER_BUTTON_OPTIONS; break; // KEY_PLUS + // FIXME + // case 11: state->buttons |= CHIAKI_CONTROLLER_BUTTON_SHARE; break; // KEY_MINUS + case 11: state->buttons |= CHIAKI_CONTROLLER_BUTTON_PS; break; // KEY_MINUS + default: + ret = false; + } + break; + case SDL_JOYBUTTONUP: + // printf("Joystick %d button %d UP\n", + // event->jbutton.which, event->jbutton.button); + switch(event->jbutton.button) + { + case 0: state->buttons ^= CHIAKI_CONTROLLER_BUTTON_MOON; break; // KEY_A + case 1: state->buttons ^= CHIAKI_CONTROLLER_BUTTON_CROSS; break; // KEY_B + case 2: state->buttons ^= CHIAKI_CONTROLLER_BUTTON_PYRAMID; break; // KEY_X + case 3: state->buttons ^= CHIAKI_CONTROLLER_BUTTON_BOX; break; // KEY_Y + case 12: state->buttons ^= CHIAKI_CONTROLLER_BUTTON_DPAD_LEFT; break; // KEY_DLEFT + case 14: state->buttons ^= CHIAKI_CONTROLLER_BUTTON_DPAD_RIGHT; break; // KEY_DRIGHT + case 13: state->buttons ^= CHIAKI_CONTROLLER_BUTTON_DPAD_UP; break; // KEY_DUP + case 15: state->buttons ^= CHIAKI_CONTROLLER_BUTTON_DPAD_DOWN; break; // KEY_DDOWN + case 6: state->buttons ^= CHIAKI_CONTROLLER_BUTTON_L1; break; // KEY_L + case 7: state->buttons ^= CHIAKI_CONTROLLER_BUTTON_R1; break; // KEY_R + case 8: state->l2_state = 0x00; break; // KEY_ZL + case 9: state->r2_state = 0x00; break; // KEY_ZR + case 4: state->buttons ^= CHIAKI_CONTROLLER_BUTTON_L3; break; // KEY_LSTICK + case 5: state->buttons ^= CHIAKI_CONTROLLER_BUTTON_R3; break; // KEY_RSTICK + case 10: state->buttons ^= CHIAKI_CONTROLLER_BUTTON_OPTIONS; break; // KEY_PLUS + //case 11: state->buttons ^= CHIAKI_CONTROLLER_BUTTON_SHARE; break; // KEY_MINUS + case 11: state->buttons ^= CHIAKI_CONTROLLER_BUTTON_PS; break; // KEY_MINUS + default: + ret = false; + } + break; + default: + ret = false; + } + return ret; +} + +bool IO::InitAVCodec() +{ + CHIAKI_LOGV(this->log, "loading AVCodec"); + // set libav video context + this->codec = avcodec_find_decoder(AV_CODEC_ID_H264); + if(!this->codec) + throw Exception("H264 Codec not available"); + + this->codec_context = avcodec_alloc_context3(codec); + if(!this->codec_context) + throw Exception("Failed to alloc codec context"); + + // use rock88's mooxlight-nx optimization + // https://github.com/rock88/moonlight-nx/blob/698d138b9fdd4e483c998254484ccfb4ec829e95/src/streaming/ffmpeg/FFmpegVideoDecoder.cpp#L63 + // this->codec_context->skip_loop_filter = AVDISCARD_ALL; + this->codec_context->flags |= AV_CODEC_FLAG_LOW_DELAY; + this->codec_context->flags2 |= AV_CODEC_FLAG2_FAST; + // this->codec_context->flags2 |= AV_CODEC_FLAG2_CHUNKS; + this->codec_context->thread_type = FF_THREAD_SLICE; + this->codec_context->thread_count = 4; + + if(avcodec_open2(this->codec_context, this->codec, nullptr) < 0) + { + avcodec_free_context(&this->codec_context); + throw Exception("Failed to open codec context"); + } + return true; +} + +bool IO::InitOpenGl() +{ + CHIAKI_LOGV(this->log, "loading OpenGL"); + + if(!InitOpenGlShader()) + return false; + + if(!InitOpenGlTextures()) + return false; + + return true; +} + +bool IO::InitOpenGlTextures() +{ + CHIAKI_LOGV(this->log, "loading OpenGL textrures"); + + D(glGenTextures(PLANES_COUNT, this->tex)); + D(glGenBuffers(PLANES_COUNT, this->pbo)); + uint8_t uv_default[] = {0x7f, 0x7f}; + for(int i=0; i < PLANES_COUNT; i++) + { + D(glBindTexture(GL_TEXTURE_2D, this->tex[i])); + D(glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR)); + D(glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR)); + D(glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE)); + D(glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE)); + D(glTexImage2D(GL_TEXTURE_2D, 0, GL_R8, 1, 1, 0, GL_RED, GL_UNSIGNED_BYTE, i > 0 ? uv_default : nullptr)); + } + + D(glUseProgram(this->prog)); + // bind only as many planes as we need + const char *plane_names[] = {"plane1", "plane2", "plane3"}; + for(int i=0; i < PLANES_COUNT; i++) + D(glUniform1i(glGetUniformLocation(this->prog, plane_names[i]), i)); + + D(glGenVertexArrays(1, &this->vao)); + D(glBindVertexArray(this->vao)); + + D(glGenBuffers(1, &this->vbo)); + D(glBindBuffer(GL_ARRAY_BUFFER, this->vbo)); + D(glBufferData(GL_ARRAY_BUFFER, sizeof(vert_pos), vert_pos, GL_STATIC_DRAW)); + + D(glBindBuffer(GL_ARRAY_BUFFER, this->vbo)); + D(glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 0, nullptr)); + D(glEnableVertexAttribArray(0)); + + D(glCullFace(GL_BACK)); + D(glEnable(GL_CULL_FACE)); + D(glClearColor(0.5, 0.5, 0.5, 1.0)); + return true; +} + +GLuint IO::CreateAndCompileShader(GLenum type, const char* source) +{ + GLint success; + GLchar msg[512]; + + GLuint handle; + D(handle = glCreateShader(type)); + if (!handle) + { + CHIAKI_LOGE(this->log, "%u: cannot create shader", type); + DP(this->prog); + } + + D(glShaderSource(handle, 1, &source, nullptr)); + D(glCompileShader(handle)); + D(glGetShaderiv(handle, GL_COMPILE_STATUS, &success)); + + if (!success) + { + D(glGetShaderInfoLog(handle, sizeof(msg), nullptr, msg)); + CHIAKI_LOGE(this->log, "%u: %s\n", type, msg); + D(glDeleteShader(handle)); + } + + return handle; +} + +bool IO::InitOpenGlShader() +{ + CHIAKI_LOGV(this->log, "loading OpenGl Shaders"); + + D(this->vert = CreateAndCompileShader(GL_VERTEX_SHADER, shader_vert_glsl)); + D(this->frag = CreateAndCompileShader(GL_FRAGMENT_SHADER, yuv420p_shader_frag_glsl)); + + D(this->prog = glCreateProgram()); + + D(glAttachShader(this->prog, this->vert)); + D(glAttachShader(this->prog, this->frag)); + D(glBindAttribLocation(this->prog, 0, "pos_attr")); + D(glLinkProgram(this->prog)); + + GLint success; + D(glGetProgramiv(this->prog, GL_LINK_STATUS, &success)); + if (!success) + { + char buf[512]; + glGetProgramInfoLog(this->prog, sizeof(buf), nullptr, buf); + CHIAKI_LOGE(this->log, "OpenGL link error: %s", buf); + return false; + } + + D(glDeleteShader(this->vert)); + D(glDeleteShader(this->frag)); + + return true; +} + +inline void IO::SetOpenGlYUVPixels(AVFrame * frame) +{ + D(glUseProgram(this->prog)); + + int planes[][3] = { + // { width_divide, height_divider, data_per_pixel } + { 1, 1, 1 }, // Y + { 2, 2, 1 }, // U + { 2, 2, 1 } // V + }; + + this->mtx.lock(); + for(int i = 0; i < PLANES_COUNT; i++) + { + int width = frame->width / planes[i][0]; + int height = frame->height / planes[i][1]; + int size = width * height * planes[i][2]; + uint8_t * buf; + + D(glBindBuffer(GL_PIXEL_UNPACK_BUFFER, this->pbo[i])); + D(glBufferData(GL_PIXEL_UNPACK_BUFFER, size, nullptr, GL_STREAM_DRAW)); + D(buf = reinterpret_cast(glMapBufferRange(GL_PIXEL_UNPACK_BUFFER, 0, size, GL_MAP_WRITE_BIT | GL_MAP_INVALIDATE_BUFFER_BIT))); + if(!buf) + { + GLint data; + D(glGetBufferParameteriv(GL_PIXEL_UNPACK_BUFFER, GL_BUFFER_SIZE, &data)); + CHIAKI_LOGE(this->log, "AVOpenGLFrame failed to map PBO"); + CHIAKI_LOGE(this->log, "Info buf == %p. size %d frame %d * %d, divs %d, %d, pbo %d GL_BUFFER_SIZE %x", + buf, size, frame->width, frame->height, planes[i][0], planes[i][1], pbo[i], data); + continue; + } + + if(frame->linesize[i] == width) + { + // Y + memcpy(buf, frame->data[i], size); + } + else + { + // UV + for(int l=0; ldata[i] + frame->linesize[i] * l, + width * planes[i][2]); + } + D(glUnmapBuffer(GL_PIXEL_UNPACK_BUFFER)); + D(glBindTexture(GL_TEXTURE_2D, tex[i])); + D(glTexImage2D(GL_TEXTURE_2D, 0, GL_R8, width, height, 0, GL_RED, GL_UNSIGNED_BYTE, nullptr)); + glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0); + } + this->mtx.unlock(); + glFinish(); +} + +inline void IO::OpenGlDraw() +{ + glClear(GL_COLOR_BUFFER_BIT); + + // send to OpenGl + SetOpenGlYUVPixels(this->frame); + + //avcodec_flush_buffers(this->codec_context); + D(glBindVertexArray(this->vao)); + + for(int i=0; i< PLANES_COUNT; i++) + { + D(glActiveTexture(GL_TEXTURE0 + i)); + D(glBindTexture(GL_TEXTURE_2D, this->tex[i])); + } + + D(glDrawArrays(GL_TRIANGLE_STRIP, 0, 4)); + D(glBindVertexArray(0)); + D(glFinish()); +} + +bool IO::InitJoystick() +{ + // https://github.com/switchbrew/switch-examples/blob/master/graphics/sdl2/sdl2-simple/source/main.cpp#L57 + // open CONTROLLER_PLAYER_1 and CONTROLLER_PLAYER_2 + // when railed, both joycons are mapped to joystick #0, + // else joycons are individually mapped to joystick #0, joystick #1, ... + for (int i = 0; i < SDL_JOYSTICK_COUNT; i++) + { + this->sdl_joystick_ptr[i] = SDL_JoystickOpen(i); + if (sdl_joystick_ptr[i] == nullptr) + { + CHIAKI_LOGE(this->log, "SDL_JoystickOpen: %s\n", SDL_GetError()); + return false; + } + } + return true; +} + +bool IO::FreeJoystick() +{ + for (int i = 0; i < SDL_JOYSTICK_COUNT; i++) + { + if(SDL_JoystickGetAttached(sdl_joystick_ptr[i])) + SDL_JoystickClose(sdl_joystick_ptr[i]); + } + return true; +} + +bool IO::MainLoop(ChiakiControllerState * state) +{ + D(glUseProgram(this->prog)); + + // handle SDL events + while(SDL_PollEvent(&this->sdl_event)) + { + this->ReadGameKeys(&this->sdl_event, state); + switch(this->sdl_event.type) + { + case SDL_QUIT: + return false; + } + } + + ReadGameTouchScreen(state); + + glClearColor(0.0f, 0.0f, 0.0f, 1.0f); + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + OpenGlDraw(); + + return !this->quit; +} + diff --git a/switch/src/main.cpp b/switch/src/main.cpp new file mode 100644 index 0000000..2043b12 --- /dev/null +++ b/switch/src/main.cpp @@ -0,0 +1,175 @@ +/* + * This file is part of Chiaki. + * + * Chiaki is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Chiaki is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Chiaki. If not, see . + */ + +// chiaki modules +#include +#include + +// discover and wakeup ps4 host +// from local network +#include "discoverymanager.h" +#include "settings.h" +#include "io.h" +#include "gui.h" + +#ifdef __SWITCH__ +#include +#else +bool appletMainLoop() +{ + return true; +} +#endif + +#ifndef CHIAKI_SWITCH_ENABLE_LINUX +#define CHIAKI_ENABLE_SWITCH_NXLINK 1 +#endif + +#ifdef __SWITCH__ +// use a custom nintendo switch socket config +// chiaki requiers many threads with udp/tcp sockets +static const SocketInitConfig g_chiakiSocketInitConfig = { + .bsdsockets_version = 1, + + .tcp_tx_buf_size = 0x8000, + .tcp_rx_buf_size = 0x10000, + .tcp_tx_buf_max_size = 0x40000, + .tcp_rx_buf_max_size = 0x40000, + + .udp_tx_buf_size = 0x40000, + .udp_rx_buf_size = 0x40000, + + .sb_efficiency = 8, + + .num_bsd_sessions = 16, + .bsd_service_type = BsdServiceType_User, +}; +#endif // __SWITCH__ + +#ifdef CHIAKI_ENABLE_SWITCH_NXLINK +static int s_nxlinkSock = -1; + +static void initNxLink() +{ + // use chiaki socket config initialization + if (R_FAILED(socketInitialize(&g_chiakiSocketInitConfig))) + return; + + s_nxlinkSock = nxlinkStdio(); + if (s_nxlinkSock >= 0) + printf("initNxLink"); + else + socketExit(); +} + +static void deinitNxLink() +{ + if (s_nxlinkSock >= 0) + { + close(s_nxlinkSock); + s_nxlinkSock = -1; + } +} +#endif // CHIAKI_ENABLE_SWITCH_NXLINK + +#ifdef __SWITCH__ +extern "C" void userAppInit() +{ +#ifdef CHIAKI_ENABLE_SWITCH_NXLINK + initNxLink(); +#endif + // to load gui resources + romfsInit(); + plInitialize(PlServiceType_User); + // load socket custom config + socketInitialize(&g_chiakiSocketInitConfig); + setsysInitialize(); +} + +extern "C" void userAppExit() +{ +#ifdef CHIAKI_ENABLE_SWITCH_NXLINK + deinitNxLink(); +#endif // CHIAKI_ENABLE_SWITCH_NXLINK + socketExit(); + /* Cleanup tesla required services. */ + hidsysExit(); + pmdmntExit(); + plExit(); + + /* Cleanup default services. */ + fsExit(); + hidExit(); + appletExit(); + setsysExit(); + smExit(); +} +#endif // __SWITCH__ + +int main(int argc, char* argv[]) +{ + // init chiaki lib + ChiakiLog log; +#if defined(CHIAKI_ENABLE_SWITCH_NXLINK) || defined(CHIAKI_SWITCH_ENABLE_LINUX) +#ifdef __SWITCH__ + chiaki_log_init(&log, CHIAKI_LOG_ALL ^ CHIAKI_LOG_VERBOSE, chiaki_log_cb_print, NULL); +#else + chiaki_log_init(&log, CHIAKI_LOG_ALL, chiaki_log_cb_print, NULL); +#endif +#else + // null log for switch version + chiaki_log_init(&log, 0, chiaki_log_cb_print, NULL); +#endif + + // load chiaki lib + CHIAKI_LOGI(&log, "Loading chaki lib"); + + ChiakiErrorCode err = chiaki_lib_init(); + if(err != CHIAKI_ERR_SUCCESS) + { + CHIAKI_LOGE(&log, "Chiaki lib init failed: %s\n", chiaki_error_string(err)); + return 1; + } + + CHIAKI_LOGI(&log, "Loading SDL audio / joystick"); + if(SDL_Init( SDL_INIT_AUDIO | SDL_INIT_JOYSTICK )) + { + CHIAKI_LOGE(&log, "SDL initialization failed: %s", SDL_GetError()); + return 1; + } + + // build sdl OpenGl and AV decoders graphical interface + IO io = IO(&log); // open Input Output class + + // manage ps4 setting discovery wakeup and registration + std::map hosts; + // create host objects form config file + Settings settings = Settings(&log, &hosts); + CHIAKI_LOGI(&log, "Read chiaki settings file"); + // FIXME use GUI for config + settings.ParseFile(); + Host * host = nullptr; + + DiscoveryManager discoverymanager = DiscoveryManager(&settings); + MainApplication app = MainApplication(&hosts, &settings, &discoverymanager, &io, &log); + app.Load(); + + CHIAKI_LOGI(&log, "Quit applet"); + SDL_Quit(); + return 0; +} + diff --git a/switch/src/settings.cpp b/switch/src/settings.cpp new file mode 100644 index 0000000..b15cc7e --- /dev/null +++ b/switch/src/settings.cpp @@ -0,0 +1,561 @@ +/* + * This file is part of Chiaki. + * + * Chiaki is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Chiaki is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Chiaki. If not, see . + */ +#include + +#include +#include "settings.h" + +Host * Settings::GetOrCreateHost(std::string *host_name) +{ + bool created = false; + // update of create Host instance + if(this->hosts->find(*host_name) == hosts->end()) + { + // create host if udefined + Host h = Host(this->log, this, *host_name); + this->hosts->emplace( *host_name, h); + created = true; + } + + Host *host = &(this->hosts->at(*host_name)); + if(created) + { + // copy default settings + // to the newly created host + this->SetPSNOnlineID(host, this->global_psn_online_id); + this->SetPSNAccountID(host, this->global_psn_account_id); + this->SetVideoResolution(host, this->global_video_resolution); + this->SetVideoFPS(host, this->global_video_fps); + } + return host; +} + + +Settings::ConfigurationItem Settings::ParseLine(std::string *line, std::string *value) +{ + Settings::ConfigurationItem ci; + std::smatch m; + for(auto it = re_map.begin(); it != re_map.end(); it++) + { + if(regex_search(*line, m, it->second)) + { + ci = it->first; + *value = m[1]; + return ci; + } + } + return UNKNOWN; +} + +ChiakiLog* Settings::GetLogger() +{ + return this->log; +} + +size_t Settings::GetB64encodeSize(size_t in) +{ + // calculate base64 buffer size after encode + return ((4 * in / 3) + 3) & ~3; +} + +std::string Settings::GetPSNOnlineID(Host * host) +{ + if(host == nullptr || host->psn_online_id.length() == 0 ) + return this->global_psn_online_id; + else + return host->psn_online_id; +} + +std::string Settings::GetPSNAccountID(Host * host) +{ + if(host == nullptr || host->psn_account_id.length() == 0 ) + return this->global_psn_account_id; + else + return host->psn_account_id; +} + +void Settings::SetPSNOnlineID(Host * host, std::string psn_online_id) +{ + if(host == nullptr) + this->global_psn_online_id = psn_online_id; + else + host->psn_online_id = psn_online_id; +} + +void Settings::SetPSNAccountID(Host * host, std::string psn_account_id) +{ + if(host == nullptr) + this->global_psn_account_id = psn_account_id; + else + host->psn_account_id = psn_account_id; +} + +std::string Settings::ResolutionPresetToString(ChiakiVideoResolutionPreset resolution) +{ + switch(resolution) + { + case CHIAKI_VIDEO_RESOLUTION_PRESET_360p: + return "360p"; + case CHIAKI_VIDEO_RESOLUTION_PRESET_540p: + return "540p"; + case CHIAKI_VIDEO_RESOLUTION_PRESET_720p: + return "720p"; + case CHIAKI_VIDEO_RESOLUTION_PRESET_1080p: + return "1080p"; + } + return "UNKNOWN"; +} + +std::string Settings::FPSPresetToString(ChiakiVideoFPSPreset fps) +{ + switch(fps) + { + case CHIAKI_VIDEO_FPS_PRESET_30: + return "30"; + case CHIAKI_VIDEO_FPS_PRESET_60: + return "60"; + } + return "UNKNOWN"; +} + +ChiakiVideoResolutionPreset Settings::StringToResolutionPreset(std::string value) +{ + if (value.compare("1080p") == 0) + return CHIAKI_VIDEO_RESOLUTION_PRESET_1080p; + else if (value.compare("720p") == 0) + return CHIAKI_VIDEO_RESOLUTION_PRESET_720p; + else if (value.compare("540p") == 0) + return CHIAKI_VIDEO_RESOLUTION_PRESET_540p; + else if (value.compare("360p") == 0) + return CHIAKI_VIDEO_RESOLUTION_PRESET_360p; + + // default + CHIAKI_LOGE(this->log, "Unable to parse String resolution: %s", + value.c_str()); + + return CHIAKI_VIDEO_RESOLUTION_PRESET_720p; +} + +ChiakiVideoFPSPreset Settings::StringToFPSPreset(std::string value) +{ + if (value.compare("60") == 0) + return CHIAKI_VIDEO_FPS_PRESET_60; + else if (value.compare("30") == 0) + return CHIAKI_VIDEO_FPS_PRESET_30; + + // default + CHIAKI_LOGE(this->log, "Unable to parse String fps: %s", + value.c_str()); + + return CHIAKI_VIDEO_FPS_PRESET_30; +} + +int Settings::FPSPresetToInt(ChiakiVideoFPSPreset fps) +{ + switch(fps) + { + case CHIAKI_VIDEO_FPS_PRESET_30: + return 30; + case CHIAKI_VIDEO_FPS_PRESET_60: + return 60; + } + return 0; +} + +int Settings::ResolutionPresetToInt(ChiakiVideoResolutionPreset resolution) +{ + switch(resolution) + { + case CHIAKI_VIDEO_RESOLUTION_PRESET_360p: + return 360; + case CHIAKI_VIDEO_RESOLUTION_PRESET_540p: + return 540; + case CHIAKI_VIDEO_RESOLUTION_PRESET_720p: + return 720; + case CHIAKI_VIDEO_RESOLUTION_PRESET_1080p: + return 1080; + } + return 0; +} + +ChiakiVideoResolutionPreset Settings::GetVideoResolution(Host * host) +{ + if(host == nullptr) + return this->global_video_resolution; + else + return host->video_resolution; +} + +ChiakiVideoFPSPreset Settings::GetVideoFPS(Host * host) +{ + if(host == nullptr) + return this->global_video_fps; + else + return host->video_fps; +} + +void Settings::SetVideoResolution(Host * host, ChiakiVideoResolutionPreset value) +{ + if(host == nullptr) + this->global_video_resolution = value; + else + host->video_resolution = value; +} + +void Settings::SetVideoResolution(Host * host, std::string value) +{ + ChiakiVideoResolutionPreset p = StringToResolutionPreset(value); + this->SetVideoResolution(host, p); +} + +void Settings::SetVideoFPS(Host * host, ChiakiVideoFPSPreset value) +{ + if(host == nullptr) + this->global_video_fps = value; + else + host->video_fps = value; +} + +void Settings::SetVideoFPS(Host * host, std::string value) +{ + ChiakiVideoFPSPreset p = StringToFPSPreset(value); + this->SetVideoFPS(host, p); +} + +#ifdef CHIAKI_ENABLE_SWITCH_OVERCLOCK +int Settings::GetCPUOverclock(Host * host) +{ + if(host == nullptr) + return this->global_cpu_overclock; + else + return host->cpu_overclock; +} + +void Settings::SetCPUOverclock(Host * host, int value) +{ + int oc = OC_1326; + if(value > OC_1580) + // max OC + oc = OC_1785; + else if(OC_1580 >= value && value > OC_1326) + oc = OC_1580; + else if(OC_1326 >= value && value > OC_1220) + oc = OC_1326; + else if(OC_1220 >= value && value > OC_1020) + oc = OC_1220; + else if(OC_1020 >= value) + // no overclock + // default nintendo switch value + oc = OC_1020; + if(host == nullptr) + this->global_cpu_overclock = oc; + else + host->cpu_overclock = oc; +} + +void Settings::SetCPUOverclock(Host * host, std::string value) +{ + int v = atoi(value.c_str()); + this->SetCPUOverclock(host, v); +} +#endif + +std::string Settings::GetHostIPAddr(Host * host) +{ + if(host != nullptr) + return host->host_addr; + else + CHIAKI_LOGE(this->log, "Cannot GetHostIPAddr from nullptr host"); + return ""; +} + +std::string Settings::GetHostName(Host * host) +{ + if(host != nullptr) + return host->host_name; + else + CHIAKI_LOGE(this->log, "Cannot GetHostName from nullptr host"); + return ""; +} + +int Settings::GetHostRPKeyType(Host * host) +{ + if(host != nullptr) + return host->rp_key_type; + + CHIAKI_LOGE(this->log, "Cannot GetHostRPKeyType from nullptr host"); + return 0; +} + + +bool Settings::SetHostRPKeyType(Host * host, std::string value) +{ + if(host != nullptr) + { + // TODO Check possible rp_type values + host->rp_key_type = std::atoi(value.c_str()); + return true; + } + return false; +} + +std::string Settings::GetHostRPKey(Host * host) +{ + if(host != nullptr) + { + if(host->rp_key_data || host->registered) + { + size_t rp_key_b64_sz = this->GetB64encodeSize(0x10); + char rp_key_b64[rp_key_b64_sz + 1] = {0}; + ChiakiErrorCode err; + err = chiaki_base64_encode( + host->rp_key, 0x10, + rp_key_b64, sizeof(rp_key_b64)); + + if(CHIAKI_ERR_SUCCESS == err) + return rp_key_b64; + else + CHIAKI_LOGE(this->log, "Failed to encode rp_key to base64"); + } + } + else + CHIAKI_LOGE(this->log, "Cannot GetHostRPKey from nullptr host"); + + return ""; +} + +std::string Settings::GetHostRPRegistKey(Host * host) +{ + if(host != nullptr) + { + if(host->rp_key_data || host->registered) + { + size_t rp_regist_key_b64_sz = this->GetB64encodeSize(CHIAKI_SESSION_AUTH_SIZE); + char rp_regist_key_b64[rp_regist_key_b64_sz + 1] = {0}; + ChiakiErrorCode err; + err = chiaki_base64_encode( + (uint8_t *) host->rp_regist_key, CHIAKI_SESSION_AUTH_SIZE, + rp_regist_key_b64, sizeof(rp_regist_key_b64)); + + if(CHIAKI_ERR_SUCCESS == err) + return rp_regist_key_b64; + else + CHIAKI_LOGE(this->log, "Failed to encode rp_regist_key to base64"); + } + } + else + CHIAKI_LOGE(this->log, "Cannot GetHostRPRegistKey from nullptr host"); + + return ""; +} + +bool Settings::SetHostRPKey(Host * host, std::string rp_key_b64) +{ + if(host != nullptr) + { + size_t rp_key_sz = sizeof(host->rp_key); + ChiakiErrorCode err = chiaki_base64_decode( + rp_key_b64.c_str(), rp_key_b64.length(), + host->rp_key, &rp_key_sz); + if(CHIAKI_ERR_SUCCESS != err) + CHIAKI_LOGE(this->log, "Failed to parse RP_KEY %s (it must be a base64 encoded)", rp_key_b64.c_str()); + else + return true; + } + else + CHIAKI_LOGE(this->log, "Cannot SetHostRPKey from nullptr host"); + + return false; +} + +bool Settings::SetHostRPRegistKey(Host * host, std::string rp_regist_key_b64) +{ + if(host != nullptr) + { + size_t rp_regist_key_sz = sizeof(host->rp_regist_key); + ChiakiErrorCode err = chiaki_base64_decode( + rp_regist_key_b64.c_str(), rp_regist_key_b64.length(), + (uint8_t*) host->rp_regist_key, &rp_regist_key_sz); + if(CHIAKI_ERR_SUCCESS != err) + CHIAKI_LOGE(this->log, "Failed to parse RP_REGIST_KEY %s (it must be a base64 encoded)", rp_regist_key_b64.c_str()); + else + return true; + } + else + CHIAKI_LOGE(this->log, "Cannot SetHostRPKey from nullptr host"); + + return false; +} + +void Settings::ParseFile() +{ + CHIAKI_LOGI(this->log, "Parse config file %s", this->filename); + std::fstream config_file; + config_file.open(this->filename, std::fstream::in); + std::string line; + std::string value; + bool rp_key_b, rp_regist_key_b, rp_key_type_b; + Host *current_host = nullptr; + if(config_file.is_open()) + { + CHIAKI_LOGV(this->log, "Config file opened"); + Settings::ConfigurationItem ci; + while(getline(config_file, line)) + { + CHIAKI_LOGV(this->log, "Parse config line `%s`", line.c_str()); + // for each line loop over config regex + ci = this->ParseLine(&line, &value); + switch(ci) + { + // got to next line + case UNKNOWN: CHIAKI_LOGV(this->log, "UNKNOWN config"); break; + case HOST_NAME: + CHIAKI_LOGV(this->log, "HOST_NAME %s", value.c_str()); + // current host is in context + current_host = this->GetOrCreateHost(&value); + // all following case will edit the current_host config + break; + case HOST_IP: + CHIAKI_LOGV(this->log, "HOST_IP %s", value.c_str()); + if(current_host != nullptr) + current_host->host_addr = value; + + // reset bool flags + rp_key_b=false; + rp_regist_key_b=false; + rp_key_type_b=false; + break; + case PSN_ONLINE_ID: + CHIAKI_LOGV(this->log, "PSN_ONLINE_ID %s", value.c_str()); + // current_host == nullptr + // means we are in global ini section + // update default setting + this->SetPSNOnlineID(current_host, value); + break; + case PSN_ACCOUNT_ID: + CHIAKI_LOGV(this->log, "PSN_ACCOUNT_ID %s", value.c_str()); + this->SetPSNAccountID(current_host, value); + break; + case RP_KEY: + CHIAKI_LOGV(this->log, "RP_KEY %s", value.c_str()); + if(current_host != nullptr) + rp_key_b = this->SetHostRPKey(current_host, value); + break; + case RP_KEY_TYPE: + CHIAKI_LOGV(this->log, "RP_KEY_TYPE %s", value.c_str()); + if(current_host != nullptr) + // TODO Check possible rp_type values + rp_key_type_b = this->SetHostRPKeyType(current_host, value); + break; + case RP_REGIST_KEY: + CHIAKI_LOGV(this->log, "RP_REGIST_KEY %s", value.c_str()); + if(current_host != nullptr) + rp_regist_key_b = this->SetHostRPRegistKey(current_host, value); + break; + case VIDEO_RESOLUTION: + this->SetVideoResolution(current_host, value); + break; + case VIDEO_FPS: + this->SetVideoFPS(current_host, value); + break; + } // ci switch + if(rp_key_b && rp_regist_key_b && rp_key_type_b) + // the current host contains rp key data + current_host->rp_key_data = true; + } // is_open + config_file.close(); + } + return; +} + +int Settings::WriteFile() +{ + std::fstream config_file; + CHIAKI_LOGI(this->log, "Write config file %s", this->filename); + // flush file (trunc) + // the config file is completely overwritten + config_file.open(this->filename, std::fstream::out | std::ofstream::trunc); + std::string line; + std::string value; + + if(this->hosts == nullptr) + return -1; + + if(config_file.is_open()) + { + // save global settings + CHIAKI_LOGD(this->log, "Write Global config file %s", this->filename); + + if(this->global_video_resolution) + config_file << "video_resolution = \"" + << this->ResolutionPresetToString(this->GetVideoResolution(nullptr)) + << "\"\n"; + + if(this->global_video_fps) + config_file << "video_fps = " + << this->FPSPresetToString(this->GetVideoFPS(nullptr)) + << "\n"; + + if(this->global_psn_online_id.length()) + config_file << "psn_online_id = \"" << this->global_psn_online_id << "\"\n"; + + if(this->global_psn_account_id.length()) + config_file << "psn_account_id = \"" << this->global_psn_account_id << "\"\n"; + + // write host config in file + // loop over all configured + for(auto it = this->hosts->begin(); it != this->hosts->end(); it++ ) + { + // first is std::string + // second is Host + CHIAKI_LOGD(this->log, "Write Host config file %s", it->first.c_str()); + + config_file << "[" << it->first << "]\n" + << "host_ip = \"" << it->second.host_addr << "\"\n"; + + if(it->second.video_resolution) + config_file << "video_resolution = \"" + << this->ResolutionPresetToString(this->GetVideoResolution(&it->second)) + << "\"\n"; + + if(it->second.video_fps) + config_file << "video_fps = " + << this->FPSPresetToString(this->GetVideoFPS(&it->second)) + << "\n"; + + if(it->second.psn_online_id.length()) + config_file << "psn_online_id = \"" << it->second.psn_online_id << "\"\n"; + + if(it->second.psn_account_id.length()) + config_file << "psn_account_id = \"" << it->second.psn_account_id << "\"\n"; + + if(it->second.rp_key_data || it->second.registered) + { + char rp_key_type[33] = { 0 }; + snprintf(rp_key_type, sizeof(rp_key_type), "%d", it->second.rp_key_type); + // save registered rp key for auto login + config_file << "rp_key = \"" << this->GetHostRPKey(&it->second) << "\"\n" + << "rp_regist_key = \"" << this->GetHostRPRegistKey(&it->second) << "\"\n" + << "rp_key_type = " << rp_key_type << "\n"; + } // + config_file << "\n"; + } // for host + } // is_open + config_file.close(); + return 0; +} + diff --git a/third-party/CMakeLists.txt b/third-party/CMakeLists.txt index 14e7f28..a71988d 100644 --- a/third-party/CMakeLists.txt +++ b/third-party/CMakeLists.txt @@ -50,3 +50,74 @@ if(NOT CHIAKI_USE_SYSTEM_JERASURE) add_library(Jerasure::Jerasure ALIAS jerasure) endif() + +################## +# borealis +################## + +if(CHIAKI_ENABLE_SWITCH) + # do not include + # borealis/library/lib/switch_wrapper.c + # switch functions are in switch/src/main.cpp + set(BOREALIS_SOURCE + borealis/library/lib/extern/libretro-common/features/features_cpu.c + borealis/library/lib/extern/libretro-common/encodings/encoding_utf.c + borealis/library/lib/extern/libretro-common/compat/compat_strl.c + borealis/library/lib/extern/nxfmtwrapper/format.cpp + borealis/library/lib/extern/nanovg/nanovg.c + borealis/library/lib/extern/glad/glad.c + borealis/library/lib/scroll_view.cpp + borealis/library/lib/style.cpp + borealis/library/lib/table.cpp + borealis/library/lib/task_manager.cpp + borealis/library/lib/progress_display.cpp + borealis/library/lib/staged_applet_frame.cpp + borealis/library/lib/applet_frame.cpp + borealis/library/lib/hint.cpp + borealis/library/lib/image.cpp + borealis/library/lib/logger.cpp + borealis/library/lib/swkbd.cpp + borealis/library/lib/crash_frame.cpp + borealis/library/lib/header.cpp + borealis/library/lib/progress_spinner.cpp + borealis/library/lib/layer_view.cpp + borealis/library/lib/notification_manager.cpp + borealis/library/lib/rectangle.cpp + borealis/library/lib/application.cpp + borealis/library/lib/box_layout.cpp + borealis/library/lib/sidebar.cpp + borealis/library/lib/dropdown.cpp + borealis/library/lib/popup_frame.cpp + borealis/library/lib/repeating_task.cpp + borealis/library/lib/absolute_layout.cpp + borealis/library/lib/i18n.cpp + borealis/library/lib/tab_frame.cpp + borealis/library/lib/thumbnail_frame.cpp + borealis/library/lib/animations.cpp + borealis/library/lib/dialog.cpp + borealis/library/lib/view.cpp + borealis/library/lib/list.cpp + borealis/library/lib/button.cpp + borealis/library/lib/label.cpp + borealis/library/lib/theme.cpp + borealis/library/lib/material_icon.cpp) + + add_library(borealis STATIC ${BOREALIS_SOURCE}) + target_include_directories(borealis PUBLIC + borealis/library/include + borealis/library/include/borealis/extern + borealis/library/include/borealis/extern/glad + borealis/library/include/borealis/extern/nanovg + borealis/library/include/borealis/extern/libretro-common + borealis/library/lib/extern/fmt/include) + + find_package(glfw3 REQUIRED) + find_library(EGL EGL) + find_library(GLAPI glapi) + find_library(DRM_NOUVEAU drm_nouveau) + target_link_libraries(borealis + glfw + ${EGL} + ${GLAPI} + ${DRM_NOUVEAU}) +endif() diff --git a/third-party/borealis b/third-party/borealis new file mode 160000 index 0000000..205e97a --- /dev/null +++ b/third-party/borealis @@ -0,0 +1 @@ +Subproject commit 205e97ab45922fa7f5c9fa6a85d5d686cd50b669 From 8dcaad3978316dac21c24ff3c54d8475bc1a2430 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Sun, 25 Oct 2020 14:32:44 +0100 Subject: [PATCH 053/237] Add Info about TG/IRC Channels --- README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/README.md b/README.md index e710b7c..dd5b15e 100644 --- a/README.md +++ b/README.md @@ -74,6 +74,13 @@ Settings -> Remote Play (ensure this is ticked) -> Add Device You can now double-click your PS4 in Chiaki's main window to start Remote Play. +## Joining the Community or Getting Help + +There are official groups for Chiaki on Telegram and IRC. They are bridged so you can join whichever you like: + +- **Telegram:** https://t.me/chiakitg +- **IRC:** #chiaki on irc.freenode.net + ## Acknowledgements This project has only been made possible because of the following Open Source projects: From 6e063109af10261accf23d0ef3a4ad82ce6c899c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Sun, 25 Oct 2020 14:35:44 +0100 Subject: [PATCH 054/237] Update Issue Templates --- .github/ISSUE_TEMPLATE/bug_report.md | 2 ++ .github/ISSUE_TEMPLATE/feature_request.md | 2 ++ 2 files changed, 4 insertions(+) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 96380c0..3168563 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -9,6 +9,8 @@ assignees: '' diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index 78e1101..c874dee 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -9,6 +9,8 @@ assignees: '' From cbfa49551d725b82b89fde665857d67bc907db64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Sun, 25 Oct 2020 21:08:13 +0100 Subject: [PATCH 055/237] Add option to use system Nanopb (#352) --- .github/workflows/bullseye.yml | 2 +- CMakeLists.txt | 13 +++++++++++++ cmake/FindNanopb.cmake | 25 +++++++++++++++++++++++++ lib/CMakeLists.txt | 3 +-- scripts/Dockerfile.bullseye | 2 +- third-party/CMakeLists.txt | 20 +++++++++++--------- 6 files changed, 52 insertions(+), 13 deletions(-) create mode 100644 cmake/FindNanopb.cmake diff --git a/.github/workflows/bullseye.yml b/.github/workflows/bullseye.yml index cc736a3..5afd2c7 100644 --- a/.github/workflows/bullseye.yml +++ b/.github/workflows/bullseye.yml @@ -18,5 +18,5 @@ jobs: - name: Docker Build run: cd scripts && docker build -t chiaki-bullseye . -f Dockerfile.bullseye && cd .. - name: Build and Test - run: docker run --rm -v "`pwd`:/build" -t chiaki-bullseye /bin/bash -c "cd /build && scripts/ci-script -DCHIAKI_USE_SYSTEM_JERASURE=ON" + run: docker run --rm -v "`pwd`:/build" -t chiaki-bullseye /bin/bash -c "cd /build && scripts/ci-script -DCHIAKI_USE_SYSTEM_JERASURE=ON -DCHIAKI_USE_SYSTEM_NANOPB=ON" diff --git a/CMakeLists.txt b/CMakeLists.txt index 1642cbd..57d4c0e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -21,6 +21,7 @@ option(CHIAKI_LIB_OPENSSL_EXTERNAL_PROJECT "Use OpenSSL as CMake external projec option(CHIAKI_GUI_ENABLE_SDL_GAMECONTROLLER "Use SDL Gamecontroller for Input" ON) option(CHIAKI_CLI_ARGP_STANDALONE "Search for standalone argp lib for CLI" OFF) tri_option(CHIAKI_USE_SYSTEM_JERASURE "Use system-provided jerasure instead of submodule" AUTO) +tri_option(CHIAKI_USE_SYSTEM_NANOPB "Use system-provided nanopb instead of submodule" AUTO) set(CHIAKI_VERSION_MAJOR 1) set(CHIAKI_VERSION_MINOR 3) @@ -74,6 +75,18 @@ if(CHIAKI_USE_SYSTEM_JERASURE) endif() endif() +find_package(PythonInterp 3 REQUIRED) # Make sure nanopb doesn't find Python 2.7 because Python 2 should just die. + +if(CHIAKI_USE_SYSTEM_NANOPB) + if(CHIAKI_USE_SYSTEM_NANOPB STREQUAL AUTO) + find_package(Nanopb QUIET) + set(CHIAKI_USE_SYSTEM_NANOPB ${Nanopb_FOUND}) + else() + find_package(Nanopb REQUIRED) + set(CHIAKI_USE_SYSTEM_NANOPB ON) + endif() +endif() + add_subdirectory(third-party) add_definitions(-DCHIAKI_VERSION_MAJOR=${CHIAKI_VERSION_MAJOR} -DCHIAKI_VERSION_MINOR=${CHIAKI_VERSION_MINOR} -DCHIAKI_VERSION_PATCH=${CHIAKI_VERSION_PATCH} -DCHIAKI_VERSION=\"${CHIAKI_VERSION}\") diff --git a/cmake/FindNanopb.cmake b/cmake/FindNanopb.cmake new file mode 100644 index 0000000..6a9eab3 --- /dev/null +++ b/cmake/FindNanopb.cmake @@ -0,0 +1,25 @@ +# Provides Nanopb::nanopb and NANOPB_GENERATOR_PY + +find_package(nanopb CONFIG) +find_file(NANOPB_GENERATOR_PY nanopb_generator.py PATH_SUFFIXES bin) + +find_path(Jerasure_INCLUDE_DIR NAMES pb_encode.h pb_decode.h) +find_library(Jerasure_LIBRARY NAMES Jerasure) + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(Nanopb + FOUND_VAR Nanopb_FOUND + REQUIRED_VARS + nanopb_FOUND + NANOPB_GENERATOR_PY +) + +if(Nanopb_FOUND) + if(NOT TARGET Nanopb::nanopb) + add_library(Nanopb::nanopb ALIAS nanopb::protobuf-nanopb-static) + set_target_properties(Jerasure::Jerasure PROPERTIES + IMPORTED_LOCATION "${Jerasure_LIBRARY}" + INTERFACE_INCLUDE_DIRECTORIES "${Jerasure_INCLUDE_DIRS}" + ) + endif() +endif() diff --git a/lib/CMakeLists.txt b/lib/CMakeLists.txt index f7083eb..a564577 100644 --- a/lib/CMakeLists.txt +++ b/lib/CMakeLists.txt @@ -74,7 +74,6 @@ set(SOURCE_FILES src/opusdecoder.c) add_subdirectory(protobuf) -include_directories("${NANOPB_SOURCE_DIR}") set_source_files_properties(${CHIAKI_LIB_PROTO_SOURCE_FILES} ${CHIAKI_LIB_PROTO_HEADER_FILES} PROPERTIES GENERATED TRUE) include_directories("${CHIAKI_LIB_PROTO_INCLUDE_DIR}") @@ -113,7 +112,7 @@ else() target_link_libraries(chiaki-lib OpenSSL::Crypto) endif() -target_link_libraries(chiaki-lib protobuf-nanopb-static) +target_link_libraries(chiaki-lib Nanopb::nanopb) target_link_libraries(chiaki-lib Jerasure::Jerasure) if(CHIAKI_LIB_ENABLE_OPUS) diff --git a/scripts/Dockerfile.bullseye b/scripts/Dockerfile.bullseye index 443c641..4946fd0 100644 --- a/scripts/Dockerfile.bullseye +++ b/scripts/Dockerfile.bullseye @@ -3,7 +3,7 @@ FROM debian:bullseye RUN apt-get update RUN apt-get -y install git g++ cmake pkg-config \ - libjerasure-dev libnanopb-dev libavcodec-dev libopus-dev \ + libjerasure-dev nanopb libnanopb-dev libavcodec-dev libopus-dev \ libssl-dev protobuf-compiler python3 python3-protobuf \ libevdev-dev libudev-dev \ qt5-default libqt5svg5-dev qtmultimedia5-dev libsdl2-dev diff --git a/third-party/CMakeLists.txt b/third-party/CMakeLists.txt index a71988d..33fbfbc 100644 --- a/third-party/CMakeLists.txt +++ b/third-party/CMakeLists.txt @@ -1,14 +1,16 @@ -################## -# nanopb -################## +if(NOT CHIAKI_USE_SYSTEM_NANOPB) + ################## + # nanopb + ################## -find_package(PythonInterp 3 REQUIRED) # Make sure nanopb doesn't find Python 2.7 because Python 2 should just die. - -add_subdirectory(nanopb EXCLUDE_FROM_ALL) -set(NANOPB_SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/nanopb") -set(NANOPB_SOURCE_DIR "${NANOPB_SOURCE_DIR}" PARENT_SCOPE) -set(NANOPB_GENERATOR_PY "${NANOPB_SOURCE_DIR}/generator/nanopb_generator.py" PARENT_SCOPE) + add_subdirectory(nanopb EXCLUDE_FROM_ALL) + set(NANOPB_GENERATOR_PY "${CMAKE_CURRENT_SOURCE_DIR}/nanopb/generator/nanopb_generator.py" PARENT_SCOPE) + add_library(nanopb INTERFACE) + target_link_libraries(nanopb INTERFACE protobuf-nanopb-static) + target_include_directories(nanopb INTERFACE "${CMAKE_CURRENT_SOURCE_DIR}/nanopb") + add_library(Nanopb::nanopb ALIAS nanopb) +endif() if(NOT CHIAKI_USE_SYSTEM_JERASURE) ################## From cbc17691c82769c9dd591482cabd211a1b104e74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Mon, 26 Oct 2020 10:13:52 +0100 Subject: [PATCH 056/237] Add OpenSSL exemption --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index dd5b15e..3707395 100644 --- a/README.md +++ b/README.md @@ -101,7 +101,8 @@ Created by Florian Märkl. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. +(at your option) any later version, with the additional exemption +that compiling, linking, and/or using OpenSSL is allowed. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of From b70455d19b19a366798a9b97375d78c63682b132 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Mon, 26 Oct 2020 18:56:13 +0100 Subject: [PATCH 057/237] Use SPDX Headers (#354) --- COPYING | 11 + LICENSES/GPL-3.0-or-later-OpenSSL.txt | 685 ++++++++++++++++++ README.md | 11 + android/app/src/main/cpp/audio-decoder.c | 17 +- android/app/src/main/cpp/audio-decoder.h | 17 +- android/app/src/main/cpp/audio-output.cpp | 17 +- android/app/src/main/cpp/audio-output.h | 17 +- android/app/src/main/cpp/chiaki-jni.c | 17 +- android/app/src/main/cpp/chiaki-jni.h | 17 +- android/app/src/main/cpp/circular-buf.hpp | 17 +- android/app/src/main/cpp/log.c | 17 +- android/app/src/main/cpp/log.h | 17 +- android/app/src/main/cpp/video-decoder.c | 17 +- android/app/src/main/cpp/video-decoder.h | 17 +- .../com/metallic/chiaki/common/AppDatabase.kt | 17 +- .../com/metallic/chiaki/common/DisplayHost.kt | 17 +- .../com/metallic/chiaki/common/LogManager.kt | 17 +- .../com/metallic/chiaki/common/MacAddress.kt | 17 +- .../com/metallic/chiaki/common/ManualHost.kt | 17 +- .../com/metallic/chiaki/common/Preferences.kt | 17 +- .../metallic/chiaki/common/RegisteredHost.kt | 17 +- .../chiaki/common/SerializedSettings.kt | 17 +- .../chiaki/common/ext/RevealActivity.kt | 17 +- .../metallic/chiaki/common/ext/RxLiveData.kt | 17 +- .../metallic/chiaki/common/ext/StringHex.kt | 17 +- .../chiaki/common/ext/ViewGroupInflate.kt | 17 +- .../chiaki/discovery/DiscoveryManager.kt | 17 +- .../main/DisplayHostRecyclerViewAdapter.kt | 17 +- .../FloatingActionButtonBackgroundBehavior.kt | 17 +- .../FloatingActionButtonSpeedDialBehavior.kt | 17 +- .../com/metallic/chiaki/main/MainActivity.kt | 17 +- .../com/metallic/chiaki/main/MainViewModel.kt | 17 +- .../EditManualConsoleActivity.kt | 17 +- .../EditManualConsoleViewModel.kt | 17 +- .../com/metallic/chiaki/regist/ChiakiRxLog.kt | 17 +- .../metallic/chiaki/regist/RegistActivity.kt | 17 +- .../chiaki/regist/RegistExecuteActivity.kt | 17 +- .../chiaki/regist/RegistExecuteViewModel.kt | 17 +- .../metallic/chiaki/regist/RegistViewModel.kt | 17 +- .../metallic/chiaki/session/StreamSession.kt | 17 +- .../chiaki/settings/ItemTouchSwipeCallback.kt | 17 +- .../chiaki/settings/SettingsActivity.kt | 17 +- .../chiaki/settings/SettingsFragment.kt | 17 +- .../chiaki/settings/SettingsLogsAdapter.kt | 17 +- .../chiaki/settings/SettingsLogsFragment.kt | 17 +- .../chiaki/settings/SettingsLogsViewModel.kt | 17 +- .../SettingsRegisteredHostsAdapter.kt | 17 +- .../SettingsRegisteredHostsFragment.kt | 17 +- .../SettingsRegisteredHostsViewModel.kt | 17 +- .../chiaki/settings/SettingsViewModel.kt | 17 +- .../metallic/chiaki/stream/StreamActivity.kt | 17 +- .../metallic/chiaki/stream/StreamViewModel.kt | 17 +- .../chiaki/touchcontrols/AnalogStickView.kt | 17 +- .../chiaki/touchcontrols/ButtonView.kt | 17 +- .../touchcontrols/ControlsBackgroundView.kt | 17 +- .../metallic/chiaki/touchcontrols/DPadView.kt | 17 +- .../touchcontrols/TouchControlsFragment.kt | 17 +- .../chiaki/touchcontrols/TouchTracker.kt | 17 +- .../touchcontrols/TouchpadOnlyFragment.kt | 17 +- .../metallic/chiaki/touchcontrols/Vector.kt | 17 +- cli/include/chiaki-cli.h | 17 +- cli/src/discover.c | 17 +- cli/src/wakeup.c | 17 +- gui/include/avopenglframeuploader.h | 17 +- gui/include/avopenglwidget.h | 17 +- gui/include/controllermanager.h | 17 +- gui/include/discoverymanager.h | 17 +- gui/include/dynamicgridwidget.h | 17 +- gui/include/exception.h | 17 +- gui/include/host.h | 17 +- gui/include/loginpindialog.h | 17 +- gui/include/mainwindow.h | 17 +- gui/include/manualhostdialog.h | 17 +- gui/include/registdialog.h | 17 +- gui/include/servericonwidget.h | 17 +- gui/include/serveritemwidget.h | 17 +- gui/include/sessionlog.h | 17 +- gui/include/settings.h | 17 +- gui/include/settingsdialog.h | 17 +- gui/include/settingskeycapturedialog.h | 17 +- gui/include/streamsession.h | 17 +- gui/include/streamwindow.h | 17 +- gui/include/videodecoder.h | 17 +- gui/src/avopenglframeuploader.cpp | 17 +- gui/src/avopenglwidget.cpp | 17 +- gui/src/controllermanager.cpp | 17 +- gui/src/discoverymanager.cpp | 17 +- gui/src/dynamicgridwidget.cpp | 17 +- gui/src/host.cpp | 17 +- gui/src/loginpindialog.cpp | 17 +- gui/src/mainwindow.cpp | 17 +- gui/src/manualhostdialog.cpp | 17 +- gui/src/registdialog.cpp | 17 +- gui/src/servericonwidget.cpp | 17 +- gui/src/serveritemwidget.cpp | 17 +- gui/src/sessionlog.cpp | 17 +- gui/src/settings.cpp | 17 +- gui/src/settingsdialog.cpp | 17 +- gui/src/settingskeycapturedialog.cpp | 17 +- gui/src/streamsession.cpp | 17 +- gui/src/streamwindow.cpp | 17 +- gui/src/videodecoder.cpp | 17 +- lib/config.h.in | 17 +- lib/include/chiaki/audio.h | 17 +- lib/include/chiaki/audioreceiver.h | 17 +- lib/include/chiaki/base64.h | 17 +- lib/include/chiaki/common.h | 17 +- lib/include/chiaki/congestioncontrol.h | 17 +- lib/include/chiaki/controller.h | 17 +- lib/include/chiaki/ctrl.h | 17 +- lib/include/chiaki/discovery.h | 17 +- lib/include/chiaki/discoveryservice.h | 17 +- lib/include/chiaki/ecdh.h | 17 +- lib/include/chiaki/fec.h | 17 +- lib/include/chiaki/feedback.h | 17 +- lib/include/chiaki/feedbacksender.h | 17 +- lib/include/chiaki/frameprocessor.h | 17 +- lib/include/chiaki/gkcrypt.h | 17 +- lib/include/chiaki/http.h | 17 +- lib/include/chiaki/launchspec.h | 17 +- lib/include/chiaki/log.h | 17 +- lib/include/chiaki/opusdecoder.h | 17 +- lib/include/chiaki/random.h | 17 +- lib/include/chiaki/regist.h | 17 +- lib/include/chiaki/reorderqueue.h | 17 +- lib/include/chiaki/rpcrypt.h | 17 +- lib/include/chiaki/senkusha.h | 17 +- lib/include/chiaki/seqnum.h | 17 +- lib/include/chiaki/session.h | 17 +- lib/include/chiaki/sock.h | 17 +- lib/include/chiaki/stoppipe.h | 17 +- lib/include/chiaki/streamconnection.h | 17 +- lib/include/chiaki/takion.h | 17 +- lib/include/chiaki/takionsendbuffer.h | 17 +- lib/include/chiaki/thread.h | 17 +- lib/include/chiaki/time.h | 17 +- lib/include/chiaki/video.h | 17 +- lib/include/chiaki/videoreceiver.h | 17 +- lib/src/audio.c | 17 +- lib/src/audioreceiver.c | 17 +- lib/src/base64.c | 17 +- lib/src/common.c | 17 +- lib/src/congestioncontrol.c | 17 +- lib/src/controller.c | 17 +- lib/src/ctrl.c | 17 +- lib/src/discovery.c | 17 +- lib/src/discoveryservice.c | 17 +- lib/src/ecdh.c | 17 +- lib/src/fec.c | 17 +- lib/src/feedback.c | 17 +- lib/src/feedbacksender.c | 17 +- lib/src/frameprocessor.c | 17 +- lib/src/gkcrypt.c | 17 +- lib/src/http.c | 17 +- lib/src/launchspec.c | 17 +- lib/src/log.c | 17 +- lib/src/opusdecoder.c | 17 +- lib/src/pb_utils.h | 17 +- lib/src/random.c | 17 +- lib/src/regist.c | 17 +- lib/src/reorderqueue.c | 17 +- lib/src/rpcrypt.c | 17 +- lib/src/senkusha.c | 17 +- lib/src/session.c | 17 +- lib/src/sock.c | 17 +- lib/src/stoppipe.c | 17 +- lib/src/streamconnection.c | 17 +- lib/src/takion.c | 17 +- lib/src/takionsendbuffer.c | 17 +- lib/src/thread.c | 17 +- lib/src/time.c | 17 +- lib/src/utils.h | 17 +- lib/src/videoreceiver.c | 17 +- setsu/demo/main.c | 17 +- setsu/include/setsu.h | 17 +- setsu/src/setsu.c | 17 +- switch/include/discoverymanager.h | 17 +- switch/include/exception.h | 17 +- switch/include/gui.h | 17 +- switch/include/host.h | 17 +- switch/include/io.h | 17 +- switch/include/settings.h | 17 +- switch/src/discoverymanager.cpp | 17 +- switch/src/gui.cpp | 17 +- switch/src/host.cpp | 17 +- switch/src/io.cpp | 17 +- switch/src/main.cpp | 17 +- switch/src/settings.cpp | 17 +- test/fec.c | 17 +- test/gkcrypt.c | 17 +- test/http.c | 17 +- test/keystate.c | 17 +- test/main.c | 17 +- test/regist.c | 17 +- test/reorderqueue.c | 17 +- test/rpcrypt.c | 17 +- test/seqnum.c | 17 +- test/takion.c | 17 +- test/test_log.c | 17 +- test/test_log.h | 17 +- 200 files changed, 904 insertions(+), 3152 deletions(-) create mode 100644 LICENSES/GPL-3.0-or-later-OpenSSL.txt diff --git a/COPYING b/COPYING index f288702..dbc3698 100644 --- a/COPYING +++ b/COPYING @@ -672,3 +672,14 @@ may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . + +Additional permission under GNU GPL version 3 section 7 + +If you modify this program, or any covered work, by linking or +combining it with the OpenSSL project's OpenSSL library (or a +modified version of that library), containing parts covered by the +terms of the OpenSSL or SSLeay licenses, the Free Software Foundation +grants you additional permission to convey the resulting work. +Corresponding Source for a non-source form of such a combination +shall include the source code for the parts of OpenSSL used as well +as that of the covered work. diff --git a/LICENSES/GPL-3.0-or-later-OpenSSL.txt b/LICENSES/GPL-3.0-or-later-OpenSSL.txt new file mode 100644 index 0000000..dbc3698 --- /dev/null +++ b/LICENSES/GPL-3.0-or-later-OpenSSL.txt @@ -0,0 +1,685 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. + +Additional permission under GNU GPL version 3 section 7 + +If you modify this program, or any covered work, by linking or +combining it with the OpenSSL project's OpenSSL library (or a +modified version of that library), containing parts covered by the +terms of the OpenSSL or SSLeay licenses, the Free Software Foundation +grants you additional permission to convey the resulting work. +Corresponding Source for a non-source form of such a combination +shall include the source code for the parts of OpenSSL used as well +as that of the covered work. diff --git a/README.md b/README.md index 3707395..ed1df05 100644 --- a/README.md +++ b/README.md @@ -111,3 +111,14 @@ GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . + +Additional permission under GNU GPL version 3 section 7 + +If you modify this program, or any covered work, by linking or +combining it with the OpenSSL project's OpenSSL library (or a +modified version of that library), containing parts covered by the +terms of the OpenSSL or SSLeay licenses, the Free Software Foundation +grants you additional permission to convey the resulting work. +Corresponding Source for a non-source form of such a combination +shall include the source code for the parts of OpenSSL used as well +as that of the covered work. diff --git a/android/app/src/main/cpp/audio-decoder.c b/android/app/src/main/cpp/audio-decoder.c index f7a979d..814bd83 100644 --- a/android/app/src/main/cpp/audio-decoder.c +++ b/android/app/src/main/cpp/audio-decoder.c @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL #include "audio-decoder.h" diff --git a/android/app/src/main/cpp/audio-decoder.h b/android/app/src/main/cpp/audio-decoder.h index c237b4b..1bca6cd 100644 --- a/android/app/src/main/cpp/audio-decoder.h +++ b/android/app/src/main/cpp/audio-decoder.h @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL #ifndef CHIAKI_JNI_AUDIO_DECODER_H #define CHIAKI_JNI_AUDIO_DECODER_H diff --git a/android/app/src/main/cpp/audio-output.cpp b/android/app/src/main/cpp/audio-output.cpp index 4531830..ccd9733 100644 --- a/android/app/src/main/cpp/audio-output.cpp +++ b/android/app/src/main/cpp/audio-output.cpp @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL #include "audio-output.h" diff --git a/android/app/src/main/cpp/audio-output.h b/android/app/src/main/cpp/audio-output.h index 3af3002..3cad981 100644 --- a/android/app/src/main/cpp/audio-output.h +++ b/android/app/src/main/cpp/audio-output.h @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL #ifndef CHIAKI_JNI_AUDIO_OUTPUT_H #define CHIAKI_JNI_AUDIO_OUTPUT_H diff --git a/android/app/src/main/cpp/chiaki-jni.c b/android/app/src/main/cpp/chiaki-jni.c index feab1a9..195dfe5 100644 --- a/android/app/src/main/cpp/chiaki-jni.c +++ b/android/app/src/main/cpp/chiaki-jni.c @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL #include diff --git a/android/app/src/main/cpp/chiaki-jni.h b/android/app/src/main/cpp/chiaki-jni.h index cf356bc..3c802ec 100644 --- a/android/app/src/main/cpp/chiaki-jni.h +++ b/android/app/src/main/cpp/chiaki-jni.h @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL #ifndef CHIAKI_JNI_H #define CHIAKI_JNI_H diff --git a/android/app/src/main/cpp/circular-buf.hpp b/android/app/src/main/cpp/circular-buf.hpp index 79c8000..bddc8b6 100644 --- a/android/app/src/main/cpp/circular-buf.hpp +++ b/android/app/src/main/cpp/circular-buf.hpp @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL #ifndef CHIAKI_JNI_CIRCULARBUF_HPP #define CHIAKI_JNI_CIRCULARBUF_HPP diff --git a/android/app/src/main/cpp/log.c b/android/app/src/main/cpp/log.c index 7ffc568..8962d10 100644 --- a/android/app/src/main/cpp/log.c +++ b/android/app/src/main/cpp/log.c @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL #include "log.h" diff --git a/android/app/src/main/cpp/log.h b/android/app/src/main/cpp/log.h index 3d97b6d..6edf769 100644 --- a/android/app/src/main/cpp/log.h +++ b/android/app/src/main/cpp/log.h @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL #ifndef CHIAKI_JNI_LOG_H #define CHIAKI_JNI_LOG_H diff --git a/android/app/src/main/cpp/video-decoder.c b/android/app/src/main/cpp/video-decoder.c index ecf9980..534cc89 100644 --- a/android/app/src/main/cpp/video-decoder.c +++ b/android/app/src/main/cpp/video-decoder.c @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL #include "video-decoder.h" diff --git a/android/app/src/main/cpp/video-decoder.h b/android/app/src/main/cpp/video-decoder.h index 9e9e27f..f7ecdc8 100644 --- a/android/app/src/main/cpp/video-decoder.h +++ b/android/app/src/main/cpp/video-decoder.h @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL #ifndef CHIAKI_JNI_VIDEO_DECODER_H #define CHIAKI_JNI_VIDEO_DECODER_H diff --git a/android/app/src/main/java/com/metallic/chiaki/common/AppDatabase.kt b/android/app/src/main/java/com/metallic/chiaki/common/AppDatabase.kt index 64c1288..71db6c2 100644 --- a/android/app/src/main/java/com/metallic/chiaki/common/AppDatabase.kt +++ b/android/app/src/main/java/com/metallic/chiaki/common/AppDatabase.kt @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL package com.metallic.chiaki.common diff --git a/android/app/src/main/java/com/metallic/chiaki/common/DisplayHost.kt b/android/app/src/main/java/com/metallic/chiaki/common/DisplayHost.kt index 30b42d8..658785c 100644 --- a/android/app/src/main/java/com/metallic/chiaki/common/DisplayHost.kt +++ b/android/app/src/main/java/com/metallic/chiaki/common/DisplayHost.kt @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL package com.metallic.chiaki.common diff --git a/android/app/src/main/java/com/metallic/chiaki/common/LogManager.kt b/android/app/src/main/java/com/metallic/chiaki/common/LogManager.kt index 01ed0cf..33683b3 100644 --- a/android/app/src/main/java/com/metallic/chiaki/common/LogManager.kt +++ b/android/app/src/main/java/com/metallic/chiaki/common/LogManager.kt @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL package com.metallic.chiaki.common diff --git a/android/app/src/main/java/com/metallic/chiaki/common/MacAddress.kt b/android/app/src/main/java/com/metallic/chiaki/common/MacAddress.kt index 39cc934..c0c9181 100644 --- a/android/app/src/main/java/com/metallic/chiaki/common/MacAddress.kt +++ b/android/app/src/main/java/com/metallic/chiaki/common/MacAddress.kt @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL package com.metallic.chiaki.common diff --git a/android/app/src/main/java/com/metallic/chiaki/common/ManualHost.kt b/android/app/src/main/java/com/metallic/chiaki/common/ManualHost.kt index c265848..3b5784e 100644 --- a/android/app/src/main/java/com/metallic/chiaki/common/ManualHost.kt +++ b/android/app/src/main/java/com/metallic/chiaki/common/ManualHost.kt @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL package com.metallic.chiaki.common diff --git a/android/app/src/main/java/com/metallic/chiaki/common/Preferences.kt b/android/app/src/main/java/com/metallic/chiaki/common/Preferences.kt index 780fec2..a7766b9 100644 --- a/android/app/src/main/java/com/metallic/chiaki/common/Preferences.kt +++ b/android/app/src/main/java/com/metallic/chiaki/common/Preferences.kt @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL package com.metallic.chiaki.common diff --git a/android/app/src/main/java/com/metallic/chiaki/common/RegisteredHost.kt b/android/app/src/main/java/com/metallic/chiaki/common/RegisteredHost.kt index a0c83e0..9e679fe 100644 --- a/android/app/src/main/java/com/metallic/chiaki/common/RegisteredHost.kt +++ b/android/app/src/main/java/com/metallic/chiaki/common/RegisteredHost.kt @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL package com.metallic.chiaki.common diff --git a/android/app/src/main/java/com/metallic/chiaki/common/SerializedSettings.kt b/android/app/src/main/java/com/metallic/chiaki/common/SerializedSettings.kt index 234f5db..c85bffd 100644 --- a/android/app/src/main/java/com/metallic/chiaki/common/SerializedSettings.kt +++ b/android/app/src/main/java/com/metallic/chiaki/common/SerializedSettings.kt @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL package com.metallic.chiaki.common diff --git a/android/app/src/main/java/com/metallic/chiaki/common/ext/RevealActivity.kt b/android/app/src/main/java/com/metallic/chiaki/common/ext/RevealActivity.kt index db4faac..b1d25ac 100644 --- a/android/app/src/main/java/com/metallic/chiaki/common/ext/RevealActivity.kt +++ b/android/app/src/main/java/com/metallic/chiaki/common/ext/RevealActivity.kt @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL package com.metallic.chiaki.common.ext diff --git a/android/app/src/main/java/com/metallic/chiaki/common/ext/RxLiveData.kt b/android/app/src/main/java/com/metallic/chiaki/common/ext/RxLiveData.kt index 6dc5ebf..8c1d0d5 100644 --- a/android/app/src/main/java/com/metallic/chiaki/common/ext/RxLiveData.kt +++ b/android/app/src/main/java/com/metallic/chiaki/common/ext/RxLiveData.kt @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL package com.metallic.chiaki.common.ext diff --git a/android/app/src/main/java/com/metallic/chiaki/common/ext/StringHex.kt b/android/app/src/main/java/com/metallic/chiaki/common/ext/StringHex.kt index 8a5f64b..73011e1 100644 --- a/android/app/src/main/java/com/metallic/chiaki/common/ext/StringHex.kt +++ b/android/app/src/main/java/com/metallic/chiaki/common/ext/StringHex.kt @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL package com.metallic.chiaki.common.ext diff --git a/android/app/src/main/java/com/metallic/chiaki/common/ext/ViewGroupInflate.kt b/android/app/src/main/java/com/metallic/chiaki/common/ext/ViewGroupInflate.kt index b811a5c..887d509 100644 --- a/android/app/src/main/java/com/metallic/chiaki/common/ext/ViewGroupInflate.kt +++ b/android/app/src/main/java/com/metallic/chiaki/common/ext/ViewGroupInflate.kt @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL package com.metallic.chiaki.common.ext diff --git a/android/app/src/main/java/com/metallic/chiaki/discovery/DiscoveryManager.kt b/android/app/src/main/java/com/metallic/chiaki/discovery/DiscoveryManager.kt index fceb1ad..146b779 100644 --- a/android/app/src/main/java/com/metallic/chiaki/discovery/DiscoveryManager.kt +++ b/android/app/src/main/java/com/metallic/chiaki/discovery/DiscoveryManager.kt @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL package com.metallic.chiaki.discovery diff --git a/android/app/src/main/java/com/metallic/chiaki/main/DisplayHostRecyclerViewAdapter.kt b/android/app/src/main/java/com/metallic/chiaki/main/DisplayHostRecyclerViewAdapter.kt index b37eb4f..22ceae6 100644 --- a/android/app/src/main/java/com/metallic/chiaki/main/DisplayHostRecyclerViewAdapter.kt +++ b/android/app/src/main/java/com/metallic/chiaki/main/DisplayHostRecyclerViewAdapter.kt @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL package com.metallic.chiaki.main diff --git a/android/app/src/main/java/com/metallic/chiaki/main/FloatingActionButtonBackgroundBehavior.kt b/android/app/src/main/java/com/metallic/chiaki/main/FloatingActionButtonBackgroundBehavior.kt index 5a94ed6..0776723 100644 --- a/android/app/src/main/java/com/metallic/chiaki/main/FloatingActionButtonBackgroundBehavior.kt +++ b/android/app/src/main/java/com/metallic/chiaki/main/FloatingActionButtonBackgroundBehavior.kt @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL package com.metallic.chiaki.main diff --git a/android/app/src/main/java/com/metallic/chiaki/main/FloatingActionButtonSpeedDialBehavior.kt b/android/app/src/main/java/com/metallic/chiaki/main/FloatingActionButtonSpeedDialBehavior.kt index bf9294f..20853fd 100644 --- a/android/app/src/main/java/com/metallic/chiaki/main/FloatingActionButtonSpeedDialBehavior.kt +++ b/android/app/src/main/java/com/metallic/chiaki/main/FloatingActionButtonSpeedDialBehavior.kt @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL package com.metallic.chiaki.main diff --git a/android/app/src/main/java/com/metallic/chiaki/main/MainActivity.kt b/android/app/src/main/java/com/metallic/chiaki/main/MainActivity.kt index 07c06eb..12eb19b 100644 --- a/android/app/src/main/java/com/metallic/chiaki/main/MainActivity.kt +++ b/android/app/src/main/java/com/metallic/chiaki/main/MainActivity.kt @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL package com.metallic.chiaki.main diff --git a/android/app/src/main/java/com/metallic/chiaki/main/MainViewModel.kt b/android/app/src/main/java/com/metallic/chiaki/main/MainViewModel.kt index 02940bc..95d4eee 100644 --- a/android/app/src/main/java/com/metallic/chiaki/main/MainViewModel.kt +++ b/android/app/src/main/java/com/metallic/chiaki/main/MainViewModel.kt @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL package com.metallic.chiaki.main diff --git a/android/app/src/main/java/com/metallic/chiaki/manualconsole/EditManualConsoleActivity.kt b/android/app/src/main/java/com/metallic/chiaki/manualconsole/EditManualConsoleActivity.kt index c200781..143f712 100644 --- a/android/app/src/main/java/com/metallic/chiaki/manualconsole/EditManualConsoleActivity.kt +++ b/android/app/src/main/java/com/metallic/chiaki/manualconsole/EditManualConsoleActivity.kt @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL package com.metallic.chiaki.manualconsole diff --git a/android/app/src/main/java/com/metallic/chiaki/manualconsole/EditManualConsoleViewModel.kt b/android/app/src/main/java/com/metallic/chiaki/manualconsole/EditManualConsoleViewModel.kt index 512e153..da8b5fe 100644 --- a/android/app/src/main/java/com/metallic/chiaki/manualconsole/EditManualConsoleViewModel.kt +++ b/android/app/src/main/java/com/metallic/chiaki/manualconsole/EditManualConsoleViewModel.kt @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL package com.metallic.chiaki.manualconsole diff --git a/android/app/src/main/java/com/metallic/chiaki/regist/ChiakiRxLog.kt b/android/app/src/main/java/com/metallic/chiaki/regist/ChiakiRxLog.kt index 220ac00..536fab2 100644 --- a/android/app/src/main/java/com/metallic/chiaki/regist/ChiakiRxLog.kt +++ b/android/app/src/main/java/com/metallic/chiaki/regist/ChiakiRxLog.kt @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL package com.metallic.chiaki.regist diff --git a/android/app/src/main/java/com/metallic/chiaki/regist/RegistActivity.kt b/android/app/src/main/java/com/metallic/chiaki/regist/RegistActivity.kt index 6be03f9..4762119 100644 --- a/android/app/src/main/java/com/metallic/chiaki/regist/RegistActivity.kt +++ b/android/app/src/main/java/com/metallic/chiaki/regist/RegistActivity.kt @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL package com.metallic.chiaki.regist diff --git a/android/app/src/main/java/com/metallic/chiaki/regist/RegistExecuteActivity.kt b/android/app/src/main/java/com/metallic/chiaki/regist/RegistExecuteActivity.kt index c56a86c..7bf79ee 100644 --- a/android/app/src/main/java/com/metallic/chiaki/regist/RegistExecuteActivity.kt +++ b/android/app/src/main/java/com/metallic/chiaki/regist/RegistExecuteActivity.kt @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL package com.metallic.chiaki.regist diff --git a/android/app/src/main/java/com/metallic/chiaki/regist/RegistExecuteViewModel.kt b/android/app/src/main/java/com/metallic/chiaki/regist/RegistExecuteViewModel.kt index 8be8343..02593af 100644 --- a/android/app/src/main/java/com/metallic/chiaki/regist/RegistExecuteViewModel.kt +++ b/android/app/src/main/java/com/metallic/chiaki/regist/RegistExecuteViewModel.kt @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL package com.metallic.chiaki.regist diff --git a/android/app/src/main/java/com/metallic/chiaki/regist/RegistViewModel.kt b/android/app/src/main/java/com/metallic/chiaki/regist/RegistViewModel.kt index bc14037..297e65a 100644 --- a/android/app/src/main/java/com/metallic/chiaki/regist/RegistViewModel.kt +++ b/android/app/src/main/java/com/metallic/chiaki/regist/RegistViewModel.kt @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL package com.metallic.chiaki.regist diff --git a/android/app/src/main/java/com/metallic/chiaki/session/StreamSession.kt b/android/app/src/main/java/com/metallic/chiaki/session/StreamSession.kt index aaf70aa..8c54545 100644 --- a/android/app/src/main/java/com/metallic/chiaki/session/StreamSession.kt +++ b/android/app/src/main/java/com/metallic/chiaki/session/StreamSession.kt @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL package com.metallic.chiaki.session diff --git a/android/app/src/main/java/com/metallic/chiaki/settings/ItemTouchSwipeCallback.kt b/android/app/src/main/java/com/metallic/chiaki/settings/ItemTouchSwipeCallback.kt index 1d411a9..d4f2125 100644 --- a/android/app/src/main/java/com/metallic/chiaki/settings/ItemTouchSwipeCallback.kt +++ b/android/app/src/main/java/com/metallic/chiaki/settings/ItemTouchSwipeCallback.kt @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL package com.metallic.chiaki.settings diff --git a/android/app/src/main/java/com/metallic/chiaki/settings/SettingsActivity.kt b/android/app/src/main/java/com/metallic/chiaki/settings/SettingsActivity.kt index 7ac9e24..9f1c712 100644 --- a/android/app/src/main/java/com/metallic/chiaki/settings/SettingsActivity.kt +++ b/android/app/src/main/java/com/metallic/chiaki/settings/SettingsActivity.kt @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL package com.metallic.chiaki.settings diff --git a/android/app/src/main/java/com/metallic/chiaki/settings/SettingsFragment.kt b/android/app/src/main/java/com/metallic/chiaki/settings/SettingsFragment.kt index e541a61..0558708 100644 --- a/android/app/src/main/java/com/metallic/chiaki/settings/SettingsFragment.kt +++ b/android/app/src/main/java/com/metallic/chiaki/settings/SettingsFragment.kt @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL package com.metallic.chiaki.settings diff --git a/android/app/src/main/java/com/metallic/chiaki/settings/SettingsLogsAdapter.kt b/android/app/src/main/java/com/metallic/chiaki/settings/SettingsLogsAdapter.kt index 171d329..93821e3 100644 --- a/android/app/src/main/java/com/metallic/chiaki/settings/SettingsLogsAdapter.kt +++ b/android/app/src/main/java/com/metallic/chiaki/settings/SettingsLogsAdapter.kt @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL package com.metallic.chiaki.settings diff --git a/android/app/src/main/java/com/metallic/chiaki/settings/SettingsLogsFragment.kt b/android/app/src/main/java/com/metallic/chiaki/settings/SettingsLogsFragment.kt index 82c3a15..c0c2e3d 100644 --- a/android/app/src/main/java/com/metallic/chiaki/settings/SettingsLogsFragment.kt +++ b/android/app/src/main/java/com/metallic/chiaki/settings/SettingsLogsFragment.kt @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL package com.metallic.chiaki.settings diff --git a/android/app/src/main/java/com/metallic/chiaki/settings/SettingsLogsViewModel.kt b/android/app/src/main/java/com/metallic/chiaki/settings/SettingsLogsViewModel.kt index c2c2a53..97cd214 100644 --- a/android/app/src/main/java/com/metallic/chiaki/settings/SettingsLogsViewModel.kt +++ b/android/app/src/main/java/com/metallic/chiaki/settings/SettingsLogsViewModel.kt @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL package com.metallic.chiaki.settings diff --git a/android/app/src/main/java/com/metallic/chiaki/settings/SettingsRegisteredHostsAdapter.kt b/android/app/src/main/java/com/metallic/chiaki/settings/SettingsRegisteredHostsAdapter.kt index 7cedf2a..4edeed7 100644 --- a/android/app/src/main/java/com/metallic/chiaki/settings/SettingsRegisteredHostsAdapter.kt +++ b/android/app/src/main/java/com/metallic/chiaki/settings/SettingsRegisteredHostsAdapter.kt @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL package com.metallic.chiaki.settings diff --git a/android/app/src/main/java/com/metallic/chiaki/settings/SettingsRegisteredHostsFragment.kt b/android/app/src/main/java/com/metallic/chiaki/settings/SettingsRegisteredHostsFragment.kt index 946c3ea..f83164d 100644 --- a/android/app/src/main/java/com/metallic/chiaki/settings/SettingsRegisteredHostsFragment.kt +++ b/android/app/src/main/java/com/metallic/chiaki/settings/SettingsRegisteredHostsFragment.kt @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL package com.metallic.chiaki.settings diff --git a/android/app/src/main/java/com/metallic/chiaki/settings/SettingsRegisteredHostsViewModel.kt b/android/app/src/main/java/com/metallic/chiaki/settings/SettingsRegisteredHostsViewModel.kt index 418ae4a..1df6662 100644 --- a/android/app/src/main/java/com/metallic/chiaki/settings/SettingsRegisteredHostsViewModel.kt +++ b/android/app/src/main/java/com/metallic/chiaki/settings/SettingsRegisteredHostsViewModel.kt @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL package com.metallic.chiaki.settings diff --git a/android/app/src/main/java/com/metallic/chiaki/settings/SettingsViewModel.kt b/android/app/src/main/java/com/metallic/chiaki/settings/SettingsViewModel.kt index e69662a..1811eb8 100644 --- a/android/app/src/main/java/com/metallic/chiaki/settings/SettingsViewModel.kt +++ b/android/app/src/main/java/com/metallic/chiaki/settings/SettingsViewModel.kt @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL package com.metallic.chiaki.settings diff --git a/android/app/src/main/java/com/metallic/chiaki/stream/StreamActivity.kt b/android/app/src/main/java/com/metallic/chiaki/stream/StreamActivity.kt index a7913a4..8d3412f 100644 --- a/android/app/src/main/java/com/metallic/chiaki/stream/StreamActivity.kt +++ b/android/app/src/main/java/com/metallic/chiaki/stream/StreamActivity.kt @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL package com.metallic.chiaki.stream diff --git a/android/app/src/main/java/com/metallic/chiaki/stream/StreamViewModel.kt b/android/app/src/main/java/com/metallic/chiaki/stream/StreamViewModel.kt index db95e0e..8709090 100644 --- a/android/app/src/main/java/com/metallic/chiaki/stream/StreamViewModel.kt +++ b/android/app/src/main/java/com/metallic/chiaki/stream/StreamViewModel.kt @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL package com.metallic.chiaki.stream diff --git a/android/app/src/main/java/com/metallic/chiaki/touchcontrols/AnalogStickView.kt b/android/app/src/main/java/com/metallic/chiaki/touchcontrols/AnalogStickView.kt index 563463b..f1aa815 100644 --- a/android/app/src/main/java/com/metallic/chiaki/touchcontrols/AnalogStickView.kt +++ b/android/app/src/main/java/com/metallic/chiaki/touchcontrols/AnalogStickView.kt @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL package com.metallic.chiaki.touchcontrols diff --git a/android/app/src/main/java/com/metallic/chiaki/touchcontrols/ButtonView.kt b/android/app/src/main/java/com/metallic/chiaki/touchcontrols/ButtonView.kt index 45116fd..d709bc5 100644 --- a/android/app/src/main/java/com/metallic/chiaki/touchcontrols/ButtonView.kt +++ b/android/app/src/main/java/com/metallic/chiaki/touchcontrols/ButtonView.kt @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL package com.metallic.chiaki.touchcontrols diff --git a/android/app/src/main/java/com/metallic/chiaki/touchcontrols/ControlsBackgroundView.kt b/android/app/src/main/java/com/metallic/chiaki/touchcontrols/ControlsBackgroundView.kt index 2d5b8b1..96a3fc7 100644 --- a/android/app/src/main/java/com/metallic/chiaki/touchcontrols/ControlsBackgroundView.kt +++ b/android/app/src/main/java/com/metallic/chiaki/touchcontrols/ControlsBackgroundView.kt @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL package com.metallic.chiaki.touchcontrols diff --git a/android/app/src/main/java/com/metallic/chiaki/touchcontrols/DPadView.kt b/android/app/src/main/java/com/metallic/chiaki/touchcontrols/DPadView.kt index 06829ec..eb6f52d 100644 --- a/android/app/src/main/java/com/metallic/chiaki/touchcontrols/DPadView.kt +++ b/android/app/src/main/java/com/metallic/chiaki/touchcontrols/DPadView.kt @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL package com.metallic.chiaki.touchcontrols diff --git a/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchControlsFragment.kt b/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchControlsFragment.kt index 04e0394..ece97b2 100644 --- a/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchControlsFragment.kt +++ b/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchControlsFragment.kt @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL package com.metallic.chiaki.touchcontrols diff --git a/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchTracker.kt b/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchTracker.kt index 0d82e06..cb65c33 100644 --- a/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchTracker.kt +++ b/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchTracker.kt @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL package com.metallic.chiaki.touchcontrols diff --git a/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchpadOnlyFragment.kt b/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchpadOnlyFragment.kt index 54f2c8b..36c8744 100644 --- a/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchpadOnlyFragment.kt +++ b/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchpadOnlyFragment.kt @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL package com.metallic.chiaki.touchcontrols diff --git a/android/app/src/main/java/com/metallic/chiaki/touchcontrols/Vector.kt b/android/app/src/main/java/com/metallic/chiaki/touchcontrols/Vector.kt index be1fe4d..dc3bed7 100644 --- a/android/app/src/main/java/com/metallic/chiaki/touchcontrols/Vector.kt +++ b/android/app/src/main/java/com/metallic/chiaki/touchcontrols/Vector.kt @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL package com.metallic.chiaki.touchcontrols diff --git a/cli/include/chiaki-cli.h b/cli/include/chiaki-cli.h index 13743e0..852441d 100644 --- a/cli/include/chiaki-cli.h +++ b/cli/include/chiaki-cli.h @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL #ifndef CHIAKI_CHIAKI_CLI_H #define CHIAKI_CHIAKI_CLI_H diff --git a/cli/src/discover.c b/cli/src/discover.c index 04f3f74..0f2f152 100644 --- a/cli/src/discover.c +++ b/cli/src/discover.c @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL #include diff --git a/cli/src/wakeup.c b/cli/src/wakeup.c index 06a6220..7c6213f 100644 --- a/cli/src/wakeup.c +++ b/cli/src/wakeup.c @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL #include diff --git a/gui/include/avopenglframeuploader.h b/gui/include/avopenglframeuploader.h index ff7e0d0..12c85ef 100644 --- a/gui/include/avopenglframeuploader.h +++ b/gui/include/avopenglframeuploader.h @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL #ifndef CHIAKI_AVOPENGLFRAMEUPLOADER_H #define CHIAKI_AVOPENGLFRAMEUPLOADER_H diff --git a/gui/include/avopenglwidget.h b/gui/include/avopenglwidget.h index d9ddc0d..179be64 100644 --- a/gui/include/avopenglwidget.h +++ b/gui/include/avopenglwidget.h @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL #ifndef CHIAKI_AVOPENGLWIDGET_H #define CHIAKI_AVOPENGLWIDGET_H diff --git a/gui/include/controllermanager.h b/gui/include/controllermanager.h index 952275e..a4e6f99 100644 --- a/gui/include/controllermanager.h +++ b/gui/include/controllermanager.h @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL #ifndef CHIAKI_CONTROLLERMANAGER_H #define CHIAKI_CONTROLLERMANAGER_H diff --git a/gui/include/discoverymanager.h b/gui/include/discoverymanager.h index 05cb0f7..ba0cdf4 100644 --- a/gui/include/discoverymanager.h +++ b/gui/include/discoverymanager.h @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL #ifndef CHIAKI_DISCOVERYMANAGER_H #define CHIAKI_DISCOVERYMANAGER_H diff --git a/gui/include/dynamicgridwidget.h b/gui/include/dynamicgridwidget.h index efae63f..517fe13 100644 --- a/gui/include/dynamicgridwidget.h +++ b/gui/include/dynamicgridwidget.h @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL #ifndef CHIAKI_DYNAMICGRIDWIDGET_H #define CHIAKI_DYNAMICGRIDWIDGET_H diff --git a/gui/include/exception.h b/gui/include/exception.h index 7d0a615..fd09846 100644 --- a/gui/include/exception.h +++ b/gui/include/exception.h @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL #ifndef CHIAKI_EXCEPTION_H #define CHIAKI_EXCEPTION_H diff --git a/gui/include/host.h b/gui/include/host.h index 7d143f4..1394e40 100644 --- a/gui/include/host.h +++ b/gui/include/host.h @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL #ifndef CHIAKI_HOST_H #define CHIAKI_HOST_H diff --git a/gui/include/loginpindialog.h b/gui/include/loginpindialog.h index fc6d0e5..e794e1a 100644 --- a/gui/include/loginpindialog.h +++ b/gui/include/loginpindialog.h @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL #ifndef CHIAKI_LOGINPINDIALOG_H #define CHIAKI_LOGINPINDIALOG_H diff --git a/gui/include/mainwindow.h b/gui/include/mainwindow.h index cb287b2..20e2d45 100644 --- a/gui/include/mainwindow.h +++ b/gui/include/mainwindow.h @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL #ifndef CHIAKI_MAINWINDOW_H #define CHIAKI_MAINWINDOW_H diff --git a/gui/include/manualhostdialog.h b/gui/include/manualhostdialog.h index eeb3c03..b0bd433 100644 --- a/gui/include/manualhostdialog.h +++ b/gui/include/manualhostdialog.h @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL #ifndef CHIAKI_MANUALHOSTDIALOG_H #define CHIAKI_MANUALHOSTDIALOG_H diff --git a/gui/include/registdialog.h b/gui/include/registdialog.h index fc01ca2..45a5a1d 100644 --- a/gui/include/registdialog.h +++ b/gui/include/registdialog.h @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL #ifndef CHIAKI_REGISTDIALOG_H #define CHIAKI_REGISTDIALOG_H diff --git a/gui/include/servericonwidget.h b/gui/include/servericonwidget.h index 0a714b4..a0c2a1e 100644 --- a/gui/include/servericonwidget.h +++ b/gui/include/servericonwidget.h @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL #ifndef CHIAKI_SERVERICONWIDGET_H #define CHIAKI_SERVERICONWIDGET_H diff --git a/gui/include/serveritemwidget.h b/gui/include/serveritemwidget.h index 76f4258..1b04015 100644 --- a/gui/include/serveritemwidget.h +++ b/gui/include/serveritemwidget.h @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL #ifndef CHIAKI_SERVERITEMWIDGET_H #define CHIAKI_SERVERITEMWIDGET_H diff --git a/gui/include/sessionlog.h b/gui/include/sessionlog.h index 6977460..db838bc 100644 --- a/gui/include/sessionlog.h +++ b/gui/include/sessionlog.h @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL #ifndef CHIAKI_SESSIONLOG_H #define CHIAKI_SESSIONLOG_H diff --git a/gui/include/settings.h b/gui/include/settings.h index 1088185..d2cee79 100644 --- a/gui/include/settings.h +++ b/gui/include/settings.h @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL #ifndef CHIAKI_SETTINGS_H #define CHIAKI_SETTINGS_H diff --git a/gui/include/settingsdialog.h b/gui/include/settingsdialog.h index 90235c9..a44e209 100644 --- a/gui/include/settingsdialog.h +++ b/gui/include/settingsdialog.h @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL #ifndef CHIAKI_SETTINGSDIALOG_H #define CHIAKI_SETTINGSDIALOG_H diff --git a/gui/include/settingskeycapturedialog.h b/gui/include/settingskeycapturedialog.h index a4d7b36..dbf9cb2 100644 --- a/gui/include/settingskeycapturedialog.h +++ b/gui/include/settingskeycapturedialog.h @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL #ifndef CHIAKI_SETTINGSKEYCAPTUREDIALOG_H #define CHIAKI_SETTINGSKEYCAPTUREDIALOG_H diff --git a/gui/include/streamsession.h b/gui/include/streamsession.h index 831ed6b..295998c 100644 --- a/gui/include/streamsession.h +++ b/gui/include/streamsession.h @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL #ifndef CHIAKI_STREAMSESSION_H #define CHIAKI_STREAMSESSION_H diff --git a/gui/include/streamwindow.h b/gui/include/streamwindow.h index b554cc7..63c7440 100644 --- a/gui/include/streamwindow.h +++ b/gui/include/streamwindow.h @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL #ifndef CHIAKI_GUI_STREAMWINDOW_H #define CHIAKI_GUI_STREAMWINDOW_H diff --git a/gui/include/videodecoder.h b/gui/include/videodecoder.h index 2285d06..ebbfd45 100644 --- a/gui/include/videodecoder.h +++ b/gui/include/videodecoder.h @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL #ifndef CHIAKI_VIDEODECODER_H #define CHIAKI_VIDEODECODER_H diff --git a/gui/src/avopenglframeuploader.cpp b/gui/src/avopenglframeuploader.cpp index 1c2905a..e289c53 100644 --- a/gui/src/avopenglframeuploader.cpp +++ b/gui/src/avopenglframeuploader.cpp @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL #include #include diff --git a/gui/src/avopenglwidget.cpp b/gui/src/avopenglwidget.cpp index 21b51f3..447f2e6 100644 --- a/gui/src/avopenglwidget.cpp +++ b/gui/src/avopenglwidget.cpp @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL #include #include diff --git a/gui/src/controllermanager.cpp b/gui/src/controllermanager.cpp index 2a52ea6..b0b55a1 100644 --- a/gui/src/controllermanager.cpp +++ b/gui/src/controllermanager.cpp @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL #include diff --git a/gui/src/discoverymanager.cpp b/gui/src/discoverymanager.cpp index dc86395..3601d02 100644 --- a/gui/src/discoverymanager.cpp +++ b/gui/src/discoverymanager.cpp @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL #include #include diff --git a/gui/src/dynamicgridwidget.cpp b/gui/src/dynamicgridwidget.cpp index cf330dd..dcd4439 100644 --- a/gui/src/dynamicgridwidget.cpp +++ b/gui/src/dynamicgridwidget.cpp @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL #include diff --git a/gui/src/host.cpp b/gui/src/host.cpp index f81807c..c6d36a4 100644 --- a/gui/src/host.cpp +++ b/gui/src/host.cpp @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL #include diff --git a/gui/src/loginpindialog.cpp b/gui/src/loginpindialog.cpp index 51e0836..1d9ab07 100644 --- a/gui/src/loginpindialog.cpp +++ b/gui/src/loginpindialog.cpp @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL #include diff --git a/gui/src/mainwindow.cpp b/gui/src/mainwindow.cpp index 4148cea..6c3d8e8 100644 --- a/gui/src/mainwindow.cpp +++ b/gui/src/mainwindow.cpp @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL #include #include diff --git a/gui/src/manualhostdialog.cpp b/gui/src/manualhostdialog.cpp index 9547eba..edcb882 100644 --- a/gui/src/manualhostdialog.cpp +++ b/gui/src/manualhostdialog.cpp @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL #include #include diff --git a/gui/src/registdialog.cpp b/gui/src/registdialog.cpp index 0adcbbb..39eb713 100644 --- a/gui/src/registdialog.cpp +++ b/gui/src/registdialog.cpp @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL #include #include diff --git a/gui/src/servericonwidget.cpp b/gui/src/servericonwidget.cpp index f7d60d7..3b08917 100644 --- a/gui/src/servericonwidget.cpp +++ b/gui/src/servericonwidget.cpp @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL #include diff --git a/gui/src/serveritemwidget.cpp b/gui/src/serveritemwidget.cpp index 1771478..242d010 100644 --- a/gui/src/serveritemwidget.cpp +++ b/gui/src/serveritemwidget.cpp @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL #include #include diff --git a/gui/src/sessionlog.cpp b/gui/src/sessionlog.cpp index 77664d1..3dbb17c 100644 --- a/gui/src/sessionlog.cpp +++ b/gui/src/sessionlog.cpp @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL #include #include diff --git a/gui/src/settings.cpp b/gui/src/settings.cpp index d8653aa..40bf298 100644 --- a/gui/src/settings.cpp +++ b/gui/src/settings.cpp @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL #include #include diff --git a/gui/src/settingsdialog.cpp b/gui/src/settingsdialog.cpp index 146629e..f16d110 100644 --- a/gui/src/settingsdialog.cpp +++ b/gui/src/settingsdialog.cpp @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL #include #include diff --git a/gui/src/settingskeycapturedialog.cpp b/gui/src/settingskeycapturedialog.cpp index 10e0bb3..2ff1daa 100644 --- a/gui/src/settingskeycapturedialog.cpp +++ b/gui/src/settingskeycapturedialog.cpp @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL #include "settingskeycapturedialog.h" diff --git a/gui/src/streamsession.cpp b/gui/src/streamsession.cpp index 6ac6237..a6745ef 100644 --- a/gui/src/streamsession.cpp +++ b/gui/src/streamsession.cpp @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL #include #include diff --git a/gui/src/streamwindow.cpp b/gui/src/streamwindow.cpp index e73b798..ea712cf 100644 --- a/gui/src/streamwindow.cpp +++ b/gui/src/streamwindow.cpp @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL #include #include diff --git a/gui/src/videodecoder.cpp b/gui/src/videodecoder.cpp index ba59fe0..67a4ed0 100644 --- a/gui/src/videodecoder.cpp +++ b/gui/src/videodecoder.cpp @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL #include diff --git a/lib/config.h.in b/lib/config.h.in index d7501e0..cd0a5eb 100644 --- a/lib/config.h.in +++ b/lib/config.h.in @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL #ifndef CHIAKI_CONFIG_H #define CHIAKI_CONFIG_H diff --git a/lib/include/chiaki/audio.h b/lib/include/chiaki/audio.h index 1480b96..d713181 100644 --- a/lib/include/chiaki/audio.h +++ b/lib/include/chiaki/audio.h @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL #ifndef CHIAKI_AUDIO_H #define CHIAKI_AUDIO_H diff --git a/lib/include/chiaki/audioreceiver.h b/lib/include/chiaki/audioreceiver.h index 171cabc..9a85f1d 100644 --- a/lib/include/chiaki/audioreceiver.h +++ b/lib/include/chiaki/audioreceiver.h @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL #ifndef CHIAKI_AUDIORECEIVER_H #define CHIAKI_AUDIORECEIVER_H diff --git a/lib/include/chiaki/base64.h b/lib/include/chiaki/base64.h index 92b2ca1..7f53ea5 100644 --- a/lib/include/chiaki/base64.h +++ b/lib/include/chiaki/base64.h @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL #ifndef CHIAKI_BASE64_H #define CHIAKI_BASE64_H diff --git a/lib/include/chiaki/common.h b/lib/include/chiaki/common.h index 484604e..97b02c3 100644 --- a/lib/include/chiaki/common.h +++ b/lib/include/chiaki/common.h @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL #ifndef CHIAKI_COMMON_H #define CHIAKI_COMMON_H diff --git a/lib/include/chiaki/congestioncontrol.h b/lib/include/chiaki/congestioncontrol.h index 424396a..847052f 100644 --- a/lib/include/chiaki/congestioncontrol.h +++ b/lib/include/chiaki/congestioncontrol.h @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL #ifndef CHIAKI_CONGESTIONCONTROL_H #define CHIAKI_CONGESTIONCONTROL_H diff --git a/lib/include/chiaki/controller.h b/lib/include/chiaki/controller.h index 258cd89..193f402 100644 --- a/lib/include/chiaki/controller.h +++ b/lib/include/chiaki/controller.h @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL #ifndef CHIAKI_CONTROLLER_H #define CHIAKI_CONTROLLER_H diff --git a/lib/include/chiaki/ctrl.h b/lib/include/chiaki/ctrl.h index c07c0d2..71671ec 100644 --- a/lib/include/chiaki/ctrl.h +++ b/lib/include/chiaki/ctrl.h @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL #ifndef CHIAKI_CTRL_H #define CHIAKI_CTRL_H diff --git a/lib/include/chiaki/discovery.h b/lib/include/chiaki/discovery.h index 8694ac6..9459062 100644 --- a/lib/include/chiaki/discovery.h +++ b/lib/include/chiaki/discovery.h @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL #ifndef CHIAKI_DISCOVERY_H #define CHIAKI_DISCOVERY_H diff --git a/lib/include/chiaki/discoveryservice.h b/lib/include/chiaki/discoveryservice.h index 497f413..03dce7d 100644 --- a/lib/include/chiaki/discoveryservice.h +++ b/lib/include/chiaki/discoveryservice.h @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL #ifndef CHIAKI_DISCOVERYSERVICE_H diff --git a/lib/include/chiaki/ecdh.h b/lib/include/chiaki/ecdh.h index 0185c09..e55c03f 100644 --- a/lib/include/chiaki/ecdh.h +++ b/lib/include/chiaki/ecdh.h @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL #ifndef CHIAKI_ECDH_H #define CHIAKI_ECDH_H diff --git a/lib/include/chiaki/fec.h b/lib/include/chiaki/fec.h index 658c7c0..c6b22db 100644 --- a/lib/include/chiaki/fec.h +++ b/lib/include/chiaki/fec.h @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL #ifndef CHIAKI_FEC_H #define CHIAKI_FEC_H diff --git a/lib/include/chiaki/feedback.h b/lib/include/chiaki/feedback.h index 83cb08a..11bc707 100644 --- a/lib/include/chiaki/feedback.h +++ b/lib/include/chiaki/feedback.h @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL #ifndef CHIAKI_FEEDBACK_H #define CHIAKI_FEEDBACK_H diff --git a/lib/include/chiaki/feedbacksender.h b/lib/include/chiaki/feedbacksender.h index 1e6e6c7..63eff00 100644 --- a/lib/include/chiaki/feedbacksender.h +++ b/lib/include/chiaki/feedbacksender.h @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL #ifndef CHIAKI_FEEDBACKSENDER_H #define CHIAKI_FEEDBACKSENDER_H diff --git a/lib/include/chiaki/frameprocessor.h b/lib/include/chiaki/frameprocessor.h index f296e02..65ff97d 100644 --- a/lib/include/chiaki/frameprocessor.h +++ b/lib/include/chiaki/frameprocessor.h @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL #ifndef CHIAKI_FRAMEPROCESSOR_H #define CHIAKI_FRAMEPROCESSOR_H diff --git a/lib/include/chiaki/gkcrypt.h b/lib/include/chiaki/gkcrypt.h index 4573810..2a9f6c4 100644 --- a/lib/include/chiaki/gkcrypt.h +++ b/lib/include/chiaki/gkcrypt.h @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL #ifndef CHIAKI_GKCRYPT_H #define CHIAKI_GKCRYPT_H diff --git a/lib/include/chiaki/http.h b/lib/include/chiaki/http.h index 56c3a6f..034eeb5 100644 --- a/lib/include/chiaki/http.h +++ b/lib/include/chiaki/http.h @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL #ifndef CHIAKI_HTTP_H #define CHIAKI_HTTP_H diff --git a/lib/include/chiaki/launchspec.h b/lib/include/chiaki/launchspec.h index 7772dc3..529c9af 100644 --- a/lib/include/chiaki/launchspec.h +++ b/lib/include/chiaki/launchspec.h @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL #ifndef CHIAKI_LAUNCHSPEC_H #define CHIAKI_LAUNCHSPEC_H diff --git a/lib/include/chiaki/log.h b/lib/include/chiaki/log.h index f16b6ff..a6789ee 100644 --- a/lib/include/chiaki/log.h +++ b/lib/include/chiaki/log.h @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL #ifndef CHIAKI_LOG_H #define CHIAKI_LOG_H diff --git a/lib/include/chiaki/opusdecoder.h b/lib/include/chiaki/opusdecoder.h index 4460b29..2504f37 100644 --- a/lib/include/chiaki/opusdecoder.h +++ b/lib/include/chiaki/opusdecoder.h @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL #ifndef CHIAKI_OPUSDECODER_H #define CHIAKI_OPUSDECODER_H diff --git a/lib/include/chiaki/random.h b/lib/include/chiaki/random.h index 33bb1f2..7b757eb 100644 --- a/lib/include/chiaki/random.h +++ b/lib/include/chiaki/random.h @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL #ifndef CHIAKI_RANDOM_H #define CHIAKI_RANDOM_H diff --git a/lib/include/chiaki/regist.h b/lib/include/chiaki/regist.h index d78b1ef..65617b9 100644 --- a/lib/include/chiaki/regist.h +++ b/lib/include/chiaki/regist.h @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL #ifndef CHIAKI_REGIST_H #define CHIAKI_REGIST_H diff --git a/lib/include/chiaki/reorderqueue.h b/lib/include/chiaki/reorderqueue.h index 54e6472..9cdbf72 100644 --- a/lib/include/chiaki/reorderqueue.h +++ b/lib/include/chiaki/reorderqueue.h @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL #ifndef CHIAKI_REORDERQUEUE_H #define CHIAKI_REORDERQUEUE_H diff --git a/lib/include/chiaki/rpcrypt.h b/lib/include/chiaki/rpcrypt.h index ef30abc..2f24632 100644 --- a/lib/include/chiaki/rpcrypt.h +++ b/lib/include/chiaki/rpcrypt.h @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL #ifndef CHIAKI_RPCRYPT_H #define CHIAKI_RPCRYPT_H diff --git a/lib/include/chiaki/senkusha.h b/lib/include/chiaki/senkusha.h index de1185b..f4912a7 100644 --- a/lib/include/chiaki/senkusha.h +++ b/lib/include/chiaki/senkusha.h @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL #ifndef CHIAKI_SENKUSHA_H #define CHIAKI_SENKUSHA_H diff --git a/lib/include/chiaki/seqnum.h b/lib/include/chiaki/seqnum.h index 77825b7..897603f 100644 --- a/lib/include/chiaki/seqnum.h +++ b/lib/include/chiaki/seqnum.h @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL #ifndef CHIAKI_SEQNUM_H #define CHIAKI_SEQNUM_H diff --git a/lib/include/chiaki/session.h b/lib/include/chiaki/session.h index 87d69d0..d3c7b1f 100644 --- a/lib/include/chiaki/session.h +++ b/lib/include/chiaki/session.h @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL #ifndef CHIAKI_SESSION_H #define CHIAKI_SESSION_H diff --git a/lib/include/chiaki/sock.h b/lib/include/chiaki/sock.h index c675b8c..e50ed01 100644 --- a/lib/include/chiaki/sock.h +++ b/lib/include/chiaki/sock.h @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL #ifndef CHIAKI_SOCK_H #define CHIAKI_SOCK_H diff --git a/lib/include/chiaki/stoppipe.h b/lib/include/chiaki/stoppipe.h index 1a1ab71..968da8b 100644 --- a/lib/include/chiaki/stoppipe.h +++ b/lib/include/chiaki/stoppipe.h @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL #ifndef CHIAKI_STOPPIPE_H #define CHIAKI_STOPPIPE_H diff --git a/lib/include/chiaki/streamconnection.h b/lib/include/chiaki/streamconnection.h index c8f2bea..cc55f8b 100644 --- a/lib/include/chiaki/streamconnection.h +++ b/lib/include/chiaki/streamconnection.h @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL #ifndef CHIAKI_STREAMCONNECTION_H #define CHIAKI_STREAMCONNECTION_H diff --git a/lib/include/chiaki/takion.h b/lib/include/chiaki/takion.h index b242769..a52a3fc 100644 --- a/lib/include/chiaki/takion.h +++ b/lib/include/chiaki/takion.h @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL #ifndef CHIAKI_TAKION_H #define CHIAKI_TAKION_H diff --git a/lib/include/chiaki/takionsendbuffer.h b/lib/include/chiaki/takionsendbuffer.h index 3ecff3a..74a54b2 100644 --- a/lib/include/chiaki/takionsendbuffer.h +++ b/lib/include/chiaki/takionsendbuffer.h @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL #ifndef CHIAKI_TAKIONSENDBUFFER_H #define CHIAKI_TAKIONSENDBUFFER_H diff --git a/lib/include/chiaki/thread.h b/lib/include/chiaki/thread.h index f98488c..0c441bf 100644 --- a/lib/include/chiaki/thread.h +++ b/lib/include/chiaki/thread.h @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL #ifndef CHIAKI_THREAD_H #define CHIAKI_THREAD_H diff --git a/lib/include/chiaki/time.h b/lib/include/chiaki/time.h index ef39af8..a1e6818 100644 --- a/lib/include/chiaki/time.h +++ b/lib/include/chiaki/time.h @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL #ifndef CHIAKI_TIME_H #define CHIAKI_TIME_H diff --git a/lib/include/chiaki/video.h b/lib/include/chiaki/video.h index 5555815..28ea851 100644 --- a/lib/include/chiaki/video.h +++ b/lib/include/chiaki/video.h @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL #ifndef CHIAKI_VIDEO_H #define CHIAKI_VIDEO_H diff --git a/lib/include/chiaki/videoreceiver.h b/lib/include/chiaki/videoreceiver.h index 0ce8f0c..e68d5a4 100644 --- a/lib/include/chiaki/videoreceiver.h +++ b/lib/include/chiaki/videoreceiver.h @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL #ifndef CHIAKI_VIDEORECEIVER_H #define CHIAKI_VIDEORECEIVER_H diff --git a/lib/src/audio.c b/lib/src/audio.c index 2bd4168..529e2df 100644 --- a/lib/src/audio.c +++ b/lib/src/audio.c @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL #include diff --git a/lib/src/audioreceiver.c b/lib/src/audioreceiver.c index 54f52cd..266fde1 100644 --- a/lib/src/audioreceiver.c +++ b/lib/src/audioreceiver.c @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL #include #include diff --git a/lib/src/base64.c b/lib/src/base64.c index 8e80732..297eb3e 100644 --- a/lib/src/base64.c +++ b/lib/src/base64.c @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL #include diff --git a/lib/src/common.c b/lib/src/common.c index 824ed13..5f00d07 100644 --- a/lib/src/common.c +++ b/lib/src/common.c @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL #include #include diff --git a/lib/src/congestioncontrol.c b/lib/src/congestioncontrol.c index d27c15d..390363b 100644 --- a/lib/src/congestioncontrol.c +++ b/lib/src/congestioncontrol.c @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL #include diff --git a/lib/src/controller.c b/lib/src/controller.c index 222b380..c8d97b7 100644 --- a/lib/src/controller.c +++ b/lib/src/controller.c @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL #include diff --git a/lib/src/ctrl.c b/lib/src/ctrl.c index 44ede37..d4ee356 100644 --- a/lib/src/ctrl.c +++ b/lib/src/ctrl.c @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL #include #include diff --git a/lib/src/discovery.c b/lib/src/discovery.c index e069aaa..05c2c25 100644 --- a/lib/src/discovery.c +++ b/lib/src/discovery.c @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL #include "utils.h" diff --git a/lib/src/discoveryservice.c b/lib/src/discoveryservice.c index 2373907..58f9996 100644 --- a/lib/src/discoveryservice.c +++ b/lib/src/discoveryservice.c @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL #include diff --git a/lib/src/ecdh.c b/lib/src/ecdh.c index 8549a97..12a1da5 100644 --- a/lib/src/ecdh.c +++ b/lib/src/ecdh.c @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL #include #include diff --git a/lib/src/fec.c b/lib/src/fec.c index c797e68..d3e0797 100644 --- a/lib/src/fec.c +++ b/lib/src/fec.c @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL #include diff --git a/lib/src/feedback.c b/lib/src/feedback.c index ce9a9bf..e53d192 100644 --- a/lib/src/feedback.c +++ b/lib/src/feedback.c @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL #include #include diff --git a/lib/src/feedbacksender.c b/lib/src/feedbacksender.c index adddd44..724dba4 100644 --- a/lib/src/feedbacksender.c +++ b/lib/src/feedbacksender.c @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL #include diff --git a/lib/src/frameprocessor.c b/lib/src/frameprocessor.c index adfe9cc..17d02e1 100644 --- a/lib/src/frameprocessor.c +++ b/lib/src/frameprocessor.c @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL #include #include diff --git a/lib/src/gkcrypt.c b/lib/src/gkcrypt.c index b398a28..e5eed72 100644 --- a/lib/src/gkcrypt.c +++ b/lib/src/gkcrypt.c @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL #include #include diff --git a/lib/src/http.c b/lib/src/http.c index 50c86fe..662ddac 100644 --- a/lib/src/http.c +++ b/lib/src/http.c @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL #include diff --git a/lib/src/launchspec.c b/lib/src/launchspec.c index 438ae9d..8134dd0 100644 --- a/lib/src/launchspec.c +++ b/lib/src/launchspec.c @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL #include #include diff --git a/lib/src/log.c b/lib/src/log.c index 1055be1..8678011 100644 --- a/lib/src/log.c +++ b/lib/src/log.c @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL #include diff --git a/lib/src/opusdecoder.c b/lib/src/opusdecoder.c index ccfa2b0..a171d22 100644 --- a/lib/src/opusdecoder.c +++ b/lib/src/opusdecoder.c @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL #include #if CHIAKI_LIB_ENABLE_OPUS diff --git a/lib/src/pb_utils.h b/lib/src/pb_utils.h index 4183945..d01b204 100644 --- a/lib/src/pb_utils.h +++ b/lib/src/pb_utils.h @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL #ifndef CHIAKI_PB_UTILS_H #define CHIAKI_PB_UTILS_H diff --git a/lib/src/random.c b/lib/src/random.c index e4310e4..47ac272 100644 --- a/lib/src/random.c +++ b/lib/src/random.c @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL #include diff --git a/lib/src/regist.c b/lib/src/regist.c index 6cc6e18..f56d721 100644 --- a/lib/src/regist.c +++ b/lib/src/regist.c @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL #include "utils.h" diff --git a/lib/src/reorderqueue.c b/lib/src/reorderqueue.c index 3f800ac..cee6fef 100644 --- a/lib/src/reorderqueue.c +++ b/lib/src/reorderqueue.c @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL #include diff --git a/lib/src/rpcrypt.c b/lib/src/rpcrypt.c index a791d6b..5b4bfba 100644 --- a/lib/src/rpcrypt.c +++ b/lib/src/rpcrypt.c @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL #include diff --git a/lib/src/senkusha.c b/lib/src/senkusha.c index d1049fd..efa1261 100644 --- a/lib/src/senkusha.c +++ b/lib/src/senkusha.c @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL #include #include diff --git a/lib/src/session.c b/lib/src/session.c index 4b1cc2a..c0850d8 100644 --- a/lib/src/session.c +++ b/lib/src/session.c @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL #include #include diff --git a/lib/src/sock.c b/lib/src/sock.c index f8d224e..6f9a220 100644 --- a/lib/src/sock.c +++ b/lib/src/sock.c @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL #include #include diff --git a/lib/src/stoppipe.c b/lib/src/stoppipe.c index 3262a98..57020c2 100644 --- a/lib/src/stoppipe.c +++ b/lib/src/stoppipe.c @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL #include #include diff --git a/lib/src/streamconnection.c b/lib/src/streamconnection.c index fde546c..387ffec 100644 --- a/lib/src/streamconnection.c +++ b/lib/src/streamconnection.c @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL #include diff --git a/lib/src/takion.c b/lib/src/takion.c index 9964a1d..aef97fd 100644 --- a/lib/src/takion.c +++ b/lib/src/takion.c @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL #include #include diff --git a/lib/src/takionsendbuffer.c b/lib/src/takionsendbuffer.c index 0532c75..cb878d9 100644 --- a/lib/src/takionsendbuffer.c +++ b/lib/src/takionsendbuffer.c @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL #ifndef CHIAKI_UNIT_TEST diff --git a/lib/src/thread.c b/lib/src/thread.c index 563a140..2cd11ee 100644 --- a/lib/src/thread.c +++ b/lib/src/thread.c @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL #define _GNU_SOURCE diff --git a/lib/src/time.c b/lib/src/time.c index a72293e..c64787e 100644 --- a/lib/src/time.c +++ b/lib/src/time.c @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL #include diff --git a/lib/src/utils.h b/lib/src/utils.h index 0bd86a7..ce28fb1 100644 --- a/lib/src/utils.h +++ b/lib/src/utils.h @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL #ifndef CHIAKI_UTILS_H #define CHIAKI_UTILS_H diff --git a/lib/src/videoreceiver.c b/lib/src/videoreceiver.c index fb00f8e..48cd8bc 100644 --- a/lib/src/videoreceiver.c +++ b/lib/src/videoreceiver.c @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL #include #include diff --git a/setsu/demo/main.c b/setsu/demo/main.c index a1167e7..17b3f2b 100644 --- a/setsu/demo/main.c +++ b/setsu/demo/main.c @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL #include diff --git a/setsu/include/setsu.h b/setsu/include/setsu.h index a83f1a1..1231f7e 100644 --- a/setsu/include/setsu.h +++ b/setsu/include/setsu.h @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL #ifndef _SETSU_H #define _SETSU_H diff --git a/setsu/src/setsu.c b/setsu/src/setsu.c index 00ab5bb..fb87ba1 100644 --- a/setsu/src/setsu.c +++ b/setsu/src/setsu.c @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL #include diff --git a/switch/include/discoverymanager.h b/switch/include/discoverymanager.h index 420eef1..88574e8 100644 --- a/switch/include/discoverymanager.h +++ b/switch/include/discoverymanager.h @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL #ifndef CHIAKI_DISCOVERYMANAGER_H #define CHIAKI_DISCOVERYMANAGER_H diff --git a/switch/include/exception.h b/switch/include/exception.h index a971b03..e101773 100644 --- a/switch/include/exception.h +++ b/switch/include/exception.h @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL #ifndef CHIAKI_EXCEPTION_H #define CHIAKI_EXCEPTION_H diff --git a/switch/include/gui.h b/switch/include/gui.h index 378fb2e..1226cd1 100644 --- a/switch/include/gui.h +++ b/switch/include/gui.h @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL #ifndef CHIAKI_GUI_H #define CHIAKI_GUI_H diff --git a/switch/include/host.h b/switch/include/host.h index 2a8c5e3..1d5bd9a 100644 --- a/switch/include/host.h +++ b/switch/include/host.h @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL #ifndef CHIAKI_HOST_H #define CHIAKI_HOST_H diff --git a/switch/include/io.h b/switch/include/io.h index 69ac90f..1fdd7ac 100644 --- a/switch/include/io.h +++ b/switch/include/io.h @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL #ifndef CHIAKI_IO_H #define CHIAKI_IO_H diff --git a/switch/include/settings.h b/switch/include/settings.h index abcbe46..2ede438 100644 --- a/switch/include/settings.h +++ b/switch/include/settings.h @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL #ifndef CHIAKI_SETTINGS_H #define CHIAKI_SETTINGS_H diff --git a/switch/src/discoverymanager.cpp b/switch/src/discoverymanager.cpp index a7f0ecd..cbd3ff0 100644 --- a/switch/src/discoverymanager.cpp +++ b/switch/src/discoverymanager.cpp @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL #ifdef __SWITCH__ #include diff --git a/switch/src/gui.cpp b/switch/src/gui.cpp index b3997b5..e5838d4 100644 --- a/switch/src/gui.cpp +++ b/switch/src/gui.cpp @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL #include #include "gui.h" diff --git a/switch/src/host.cpp b/switch/src/host.cpp index 2b3dcf3..e0d161a 100644 --- a/switch/src/host.cpp +++ b/switch/src/host.cpp @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL #include diff --git a/switch/src/io.cpp b/switch/src/io.cpp index c5e12aa..abf01d3 100644 --- a/switch/src/io.cpp +++ b/switch/src/io.cpp @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL #ifdef __SWITCH__ #include diff --git a/switch/src/main.cpp b/switch/src/main.cpp index 2043b12..1ff85a0 100644 --- a/switch/src/main.cpp +++ b/switch/src/main.cpp @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL // chiaki modules #include diff --git a/switch/src/settings.cpp b/switch/src/settings.cpp index b15cc7e..9e5a7ee 100644 --- a/switch/src/settings.cpp +++ b/switch/src/settings.cpp @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL #include #include diff --git a/test/fec.c b/test/fec.c index d58107b..ebbedb0 100644 --- a/test/fec.c +++ b/test/fec.c @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL #include diff --git a/test/gkcrypt.c b/test/gkcrypt.c index d0ec4ab..06ba771 100644 --- a/test/gkcrypt.c +++ b/test/gkcrypt.c @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL #include diff --git a/test/http.c b/test/http.c index bb1cf9b..fd1d896 100644 --- a/test/http.c +++ b/test/http.c @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL #include diff --git a/test/keystate.c b/test/keystate.c index 97bb6f2..f603bb8 100644 --- a/test/keystate.c +++ b/test/keystate.c @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL #include diff --git a/test/main.c b/test/main.c index 6a43def..5618b69 100644 --- a/test/main.c +++ b/test/main.c @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL #include diff --git a/test/regist.c b/test/regist.c index d2a4aee..fc2ce93 100644 --- a/test/regist.c +++ b/test/regist.c @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL #include diff --git a/test/reorderqueue.c b/test/reorderqueue.c index e2485fb..a5e4fbf 100644 --- a/test/reorderqueue.c +++ b/test/reorderqueue.c @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL #include diff --git a/test/rpcrypt.c b/test/rpcrypt.c index 8297d37..0a25770 100644 --- a/test/rpcrypt.c +++ b/test/rpcrypt.c @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL #include diff --git a/test/seqnum.c b/test/seqnum.c index 42ad6e2..7bd09b7 100644 --- a/test/seqnum.c +++ b/test/seqnum.c @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL #include diff --git a/test/takion.c b/test/takion.c index 80550e6..8e592d0 100644 --- a/test/takion.c +++ b/test/takion.c @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL #include diff --git a/test/test_log.c b/test/test_log.c index ce4a293..55d995e 100644 --- a/test/test_log.c +++ b/test/test_log.c @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL #include "test_log.h" diff --git a/test/test_log.h b/test/test_log.h index 074e97e..10a5023 100644 --- a/test/test_log.h +++ b/test/test_log.h @@ -1,19 +1,4 @@ -/* - * This file is part of Chiaki. - * - * Chiaki is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chiaki is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with Chiaki. If not, see . - */ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL #ifndef CHIAKI_TEST_LOG_H #define CHIAKI_TEST_LOG_H From 6e055d307124b370a98179333fc6cc195ec1fef9 Mon Sep 17 00:00:00 2001 From: H0neyBadger Date: Tue, 27 Oct 2020 09:08:18 +0100 Subject: [PATCH 058/237] Fix Switch Dualshock 4 touchpad resolution (#355) --- switch/src/io.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/switch/src/io.cpp b/switch/src/io.cpp index abf01d3..476f8a5 100644 --- a/switch/src/io.cpp +++ b/switch/src/io.cpp @@ -8,9 +8,9 @@ #include "io.h" -// https://github.com/matlo/GIMX/blob/3af491c3b6a89c6a76c9831f1f022a1b73a00752/shared/gimxcontroller/include/ds4.h#L112 -#define DS4_TRACKPAD_MAX_X 1919 -#define DS4_TRACKPAD_MAX_Y 919 +// https://github.com/torvalds/linux/blob/41ba50b0572e90ed3d24fe4def54567e9050bc47/drivers/hid/hid-sony.c#L2742 +#define DS4_TRACKPAD_MAX_X 1920 +#define DS4_TRACKPAD_MAX_Y 942 #define SWITCH_TOUCHSCREEN_MAX_X 1280 #define SWITCH_TOUCHSCREEN_MAX_Y 720 From 0d653def8517186790873af9983535869e69652b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Thu, 29 Oct 2020 11:43:22 +0100 Subject: [PATCH 059/237] Remove stray Jerasure stuff in FindNanopb.cmake (#357) --- cmake/FindNanopb.cmake | 7 ------- 1 file changed, 7 deletions(-) diff --git a/cmake/FindNanopb.cmake b/cmake/FindNanopb.cmake index 6a9eab3..ed923f2 100644 --- a/cmake/FindNanopb.cmake +++ b/cmake/FindNanopb.cmake @@ -3,9 +3,6 @@ find_package(nanopb CONFIG) find_file(NANOPB_GENERATOR_PY nanopb_generator.py PATH_SUFFIXES bin) -find_path(Jerasure_INCLUDE_DIR NAMES pb_encode.h pb_decode.h) -find_library(Jerasure_LIBRARY NAMES Jerasure) - include(FindPackageHandleStandardArgs) find_package_handle_standard_args(Nanopb FOUND_VAR Nanopb_FOUND @@ -17,9 +14,5 @@ find_package_handle_standard_args(Nanopb if(Nanopb_FOUND) if(NOT TARGET Nanopb::nanopb) add_library(Nanopb::nanopb ALIAS nanopb::protobuf-nanopb-static) - set_target_properties(Jerasure::Jerasure PROPERTIES - IMPORTED_LOCATION "${Jerasure_LIBRARY}" - INTERFACE_INCLUDE_DIRECTORIES "${Jerasure_INCLUDE_DIRS}" - ) endif() endif() From f633e49cb04106788fb24bbece12aa0a418caee5 Mon Sep 17 00:00:00 2001 From: Bartosz Fenski Date: Fri, 30 Oct 2020 13:18:37 +0100 Subject: [PATCH 060/237] AppStream Fixes (#358) --- gui/re.chiaki.Chiaki.appdata.xml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/gui/re.chiaki.Chiaki.appdata.xml b/gui/re.chiaki.Chiaki.appdata.xml index 938a732..5dffc02 100644 --- a/gui/re.chiaki.Chiaki.appdata.xml +++ b/gui/re.chiaki.Chiaki.appdata.xml @@ -1,7 +1,7 @@ - re.chiaki.Chiaki + re.chiaki.chiaki chiaki.desktop Chiaki Free and Open Source Client for PlayStation 4 Remote Play @@ -14,9 +14,12 @@ Chiaki is a Free and Open Source Client for PlayStation 4 Remote Play. It can be used to play in real time on a PlayStation 4 as long as there is a network connection.

- For more information and instructions how to use Chiaki, please refer to the official documentation: https://github.com/thestr4ng3r/chiaki/blob/master/README.md#usage + For more information and instructions how to use Chiaki, please refer to the official documentation: https://github.com/thestr4ng3r/chiaki/blob/master/README.md#usage

+ + + https://raw.githubusercontent.com/thestr4ng3r/chiaki/master/assets/screenshot.png From ce0d5762b022179b8245f4b9f813df65a284ec0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Fri, 30 Oct 2020 13:49:19 +0100 Subject: [PATCH 061/237] Remove AppStream --- gui/re.chiaki.Chiaki.appdata.xml | 30 ------------------------------ 1 file changed, 30 deletions(-) delete mode 100644 gui/re.chiaki.Chiaki.appdata.xml diff --git a/gui/re.chiaki.Chiaki.appdata.xml b/gui/re.chiaki.Chiaki.appdata.xml deleted file mode 100644 index 5dffc02..0000000 --- a/gui/re.chiaki.Chiaki.appdata.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - - re.chiaki.chiaki - chiaki.desktop - Chiaki - Free and Open Source Client for PlayStation 4 Remote Play - CC0-1.0 - GPL-3.0 - Florian Märkl - https://github.com/thestr4ng3r/chiaki - -

- Chiaki is a Free and Open Source Client for PlayStation 4 Remote Play. It can be used to play in real time on a PlayStation 4 as long as there is a network connection. -

-

- For more information and instructions how to use Chiaki, please refer to the official documentation: https://github.com/thestr4ng3r/chiaki/blob/master/README.md#usage -

-
- - - - - - https://raw.githubusercontent.com/thestr4ng3r/chiaki/master/assets/screenshot.png - - - chiaki@metallic.software - -
From 163fa81c03b42a91a69323bcb2f76a19cb7c0cdb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Fri, 30 Oct 2020 14:22:28 +0100 Subject: [PATCH 062/237] Remove AppStream from CMake --- gui/CMakeLists.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/gui/CMakeLists.txt b/gui/CMakeLists.txt index 44c405b..7072b83 100644 --- a/gui/CMakeLists.txt +++ b/gui/CMakeLists.txt @@ -103,4 +103,3 @@ install(TARGETS chiaki BUNDLE DESTINATION bin) install(FILES chiaki.desktop DESTINATION share/applications) install(FILES chiaki.png DESTINATION share/icons/hicolor/512x512/apps) -install(FILES re.chiaki.Chiaki.appdata.xml DESTINATION share/metainfo) From 59e66032562f1a538a56dc3cb1e5b20117565da1 Mon Sep 17 00:00:00 2001 From: Sven Scharmentke Date: Wed, 4 Nov 2020 11:45:05 +0100 Subject: [PATCH 063/237] Use ChiakiKeyState to 32bit fix key state overflow (Fix #172) (#359) --- lib/include/chiaki/gkcrypt.h | 34 ++++--- lib/include/chiaki/takion.h | 18 ++-- lib/src/ctrl.c | 1 - lib/src/gkcrypt.c | 36 +++++-- lib/src/senkusha.c | 3 + lib/src/takion.c | 104 +++++++++++++-------- test/gkcrypt.c | 78 +++++++++++++++- test/keystate.c | 49 ++++++++-- test/takion.c | 5 +- test/takion_av_packet_parse_real_video.inl | 52 ++++++----- 10 files changed, 272 insertions(+), 108 deletions(-) diff --git a/lib/include/chiaki/gkcrypt.h b/lib/include/chiaki/gkcrypt.h index 2a9f6c4..a75d35e 100644 --- a/lib/include/chiaki/gkcrypt.h +++ b/lib/include/chiaki/gkcrypt.h @@ -20,15 +20,20 @@ extern "C" { #define CHIAKI_GKCRYPT_GMAC_KEY_REFRESH_KEY_POS 45000 #define CHIAKI_GKCRYPT_GMAC_KEY_REFRESH_IV_OFFSET 44910 +typedef struct chiaki_key_state_t +{ + uint64_t prev; +} ChiakiKeyState; + typedef struct chiaki_gkcrypt_t { uint8_t index; uint8_t *key_buf; // circular buffer of the ctr mode key stream size_t key_buf_size; size_t key_buf_populated; // size of key_buf that is already populated (on startup) - size_t key_buf_key_pos_min; // minimal key pos currently in key_buf + uint64_t key_buf_key_pos_min; // minimal key pos currently in key_buf size_t key_buf_start_offset; // offset in key_buf of the minimal key pos - size_t last_key_pos; // last key pos that has been requested + uint64_t last_key_pos; // last key pos that has been requested bool key_buf_thread_stop; ChiakiMutex key_buf_mutex; ChiakiCond key_buf_cond; @@ -50,14 +55,14 @@ struct chiaki_session_t; CHIAKI_EXPORT ChiakiErrorCode chiaki_gkcrypt_init(ChiakiGKCrypt *gkcrypt, ChiakiLog *log, size_t key_buf_chunks, uint8_t index, const uint8_t *handshake_key, const uint8_t *ecdh_secret); CHIAKI_EXPORT void chiaki_gkcrypt_fini(ChiakiGKCrypt *gkcrypt); -CHIAKI_EXPORT ChiakiErrorCode chiaki_gkcrypt_gen_key_stream(ChiakiGKCrypt *gkcrypt, size_t key_pos, uint8_t *buf, size_t buf_size); -CHIAKI_EXPORT ChiakiErrorCode chiaki_gkcrypt_get_key_stream(ChiakiGKCrypt *gkcrypt, size_t key_pos, uint8_t *buf, size_t buf_size); -CHIAKI_EXPORT ChiakiErrorCode chiaki_gkcrypt_decrypt(ChiakiGKCrypt *gkcrypt, size_t key_pos, uint8_t *buf, size_t buf_size); -static inline ChiakiErrorCode chiaki_gkcrypt_encrypt(ChiakiGKCrypt *gkcrypt, size_t key_pos, uint8_t *buf, size_t buf_size) { return chiaki_gkcrypt_decrypt(gkcrypt, key_pos, buf, buf_size); } +CHIAKI_EXPORT ChiakiErrorCode chiaki_gkcrypt_gen_key_stream(ChiakiGKCrypt *gkcrypt, uint64_t key_pos, uint8_t *buf, size_t buf_size); +CHIAKI_EXPORT ChiakiErrorCode chiaki_gkcrypt_get_key_stream(ChiakiGKCrypt *gkcrypt, uint64_t key_pos, uint8_t *buf, size_t buf_size); +CHIAKI_EXPORT ChiakiErrorCode chiaki_gkcrypt_decrypt(ChiakiGKCrypt *gkcrypt, uint64_t key_pos, uint8_t *buf, size_t buf_size); +static inline ChiakiErrorCode chiaki_gkcrypt_encrypt(ChiakiGKCrypt *gkcrypt, uint64_t key_pos, uint8_t *buf, size_t buf_size) { return chiaki_gkcrypt_decrypt(gkcrypt, key_pos, buf, buf_size); } CHIAKI_EXPORT void chiaki_gkcrypt_gen_gmac_key(uint64_t index, const uint8_t *key_base, const uint8_t *iv, uint8_t *key_out); CHIAKI_EXPORT void chiaki_gkcrypt_gen_new_gmac_key(ChiakiGKCrypt *gkcrypt, uint64_t index); CHIAKI_EXPORT void chiaki_gkcrypt_gen_tmp_gmac_key(ChiakiGKCrypt *gkcrypt, uint64_t index, uint8_t *key_out); -CHIAKI_EXPORT ChiakiErrorCode chiaki_gkcrypt_gmac(ChiakiGKCrypt *gkcrypt, size_t key_pos, const uint8_t *buf, size_t buf_size, uint8_t *gmac_out); +CHIAKI_EXPORT ChiakiErrorCode chiaki_gkcrypt_gmac(ChiakiGKCrypt *gkcrypt, uint64_t key_pos, const uint8_t *buf, size_t buf_size, uint8_t *gmac_out); static inline ChiakiGKCrypt *chiaki_gkcrypt_new(ChiakiLog *log, size_t key_buf_chunks, uint8_t index, const uint8_t *handshake_key, const uint8_t *ecdh_secret) { @@ -81,12 +86,17 @@ static inline void chiaki_gkcrypt_free(ChiakiGKCrypt *gkcrypt) free(gkcrypt); } -typedef struct chiaki_key_state_t { - uint64_t prev; -} ChiakiKeyState; - CHIAKI_EXPORT void chiaki_key_state_init(ChiakiKeyState *state); -CHIAKI_EXPORT uint64_t chiaki_key_state_request_pos(ChiakiKeyState *state, uint32_t low); + +/** + * @param commit whether to remember this key_pos to update the state. Should only be true after authentication to avoid DoS. + */ +CHIAKI_EXPORT uint64_t chiaki_key_state_request_pos(ChiakiKeyState *state, uint32_t low, bool commit); + +/** + * Update the internal state after knowing that this key_pos is authentic. + */ +CHIAKI_EXPORT void chiaki_key_state_commit(ChiakiKeyState *state, uint64_t prev); #ifdef __cplusplus } diff --git a/lib/include/chiaki/takion.h b/lib/include/chiaki/takion.h index a52a3fc..566a996 100644 --- a/lib/include/chiaki/takion.h +++ b/lib/include/chiaki/takion.h @@ -24,8 +24,6 @@ extern "C" { #endif -typedef uint32_t ChiakiTakionPacketKeyPos; - typedef enum chiaki_takion_message_data_type_t { CHIAKI_TAKION_MESSAGE_DATA_TYPE_PROTOBUF = 0, CHIAKI_TAKION_MESSAGE_DATA_TYPE_9 = 9 @@ -45,7 +43,7 @@ typedef struct chiaki_takion_av_packet_t uint8_t adaptive_stream_index; uint8_t byte_at_0x2c; - uint32_t key_pos; + uint64_t key_pos; uint8_t *data; // not owned size_t data_size; @@ -55,7 +53,7 @@ static inline uint8_t chiaki_takion_av_packet_audio_unit_size(ChiakiTakionAVPack static inline uint8_t chiaki_takion_av_packet_audio_source_units_count(ChiakiTakionAVPacket *packet) { return packet->units_in_frame_fec & 0xf; } static inline uint8_t chiaki_takion_av_packet_audio_fec_units_count(ChiakiTakionAVPacket *packet) { return (packet->units_in_frame_fec >> 4) & 0xf; } -typedef ChiakiErrorCode (*ChiakiTakionAVPacketParse)(ChiakiTakionAVPacket *packet, uint8_t *buf, size_t buf_size); +typedef ChiakiErrorCode (*ChiakiTakionAVPacketParse)(ChiakiTakionAVPacket *packet, ChiakiKeyState *key_state, uint8_t *buf, size_t buf_size); typedef struct chiaki_takion_congestion_packet_t { @@ -133,7 +131,7 @@ typedef struct chiaki_takion_t size_t postponed_packets_count; ChiakiGKCrypt *gkcrypt_local; // if NULL (default), no gmac is calculated and nothing is encrypted - size_t key_pos_local; + uint64_t key_pos_local; ChiakiMutex gkcrypt_local_mutex; ChiakiGKCrypt *gkcrypt_remote; // if NULL (default), remote gmacs are IGNORED (!) and everything is expected to be unencrypted @@ -158,6 +156,8 @@ typedef struct chiaki_takion_t uint32_t a_rwnd; ChiakiTakionAVPacketParse av_packet_parse; + + ChiakiKeyState key_state; } ChiakiTakion; @@ -178,7 +178,7 @@ static inline void chiaki_takion_set_crypt(ChiakiTakion *takion, ChiakiGKCrypt * * Thread-safe while Takion is running. * @param key_pos pointer to write the new key pos to. will be 0 if encryption is disabled. Contents undefined on failure. */ -CHIAKI_EXPORT ChiakiErrorCode chiaki_takion_crypt_advance_key_pos(ChiakiTakion *takion, size_t data_size, size_t *key_pos); +CHIAKI_EXPORT ChiakiErrorCode chiaki_takion_crypt_advance_key_pos(ChiakiTakion *takion, size_t data_size, uint64_t *key_pos); /** * Send a datagram directly on the socket. @@ -193,7 +193,7 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_takion_send_raw(ChiakiTakion *takion, const * * If encryption is disabled, the MAC will be set to 0. */ -CHIAKI_EXPORT ChiakiErrorCode chiaki_takion_send(ChiakiTakion *takion, uint8_t *buf, size_t buf_size); +CHIAKI_EXPORT ChiakiErrorCode chiaki_takion_send(ChiakiTakion *takion, uint8_t *buf, size_t buf_size, uint64_t key_pos); /** * Thread-safe while Takion is running. @@ -220,7 +220,7 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_takion_send_feedback_history(ChiakiTakion * #define CHIAKI_TAKION_V9_AV_HEADER_SIZE_VIDEO 0x17 #define CHIAKI_TAKION_V9_AV_HEADER_SIZE_AUDIO 0x12 -CHIAKI_EXPORT ChiakiErrorCode chiaki_takion_v9_av_packet_parse(ChiakiTakionAVPacket *packet, uint8_t *buf, size_t buf_size); +CHIAKI_EXPORT ChiakiErrorCode chiaki_takion_v9_av_packet_parse(ChiakiTakionAVPacket *packet, ChiakiKeyState *key_state, uint8_t *buf, size_t buf_size); #define CHIAKI_TAKION_V7_AV_HEADER_SIZE_BASE 0x12 #define CHIAKI_TAKION_V7_AV_HEADER_SIZE_VIDEO_ADD 0x3 @@ -228,7 +228,7 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_takion_v9_av_packet_parse(ChiakiTakionAVPac CHIAKI_EXPORT ChiakiErrorCode chiaki_takion_v7_av_packet_format_header(uint8_t *buf, size_t buf_size, size_t *header_size_out, ChiakiTakionAVPacket *packet); -CHIAKI_EXPORT ChiakiErrorCode chiaki_takion_v7_av_packet_parse(ChiakiTakionAVPacket *packet, uint8_t *buf, size_t buf_size); +CHIAKI_EXPORT ChiakiErrorCode chiaki_takion_v7_av_packet_parse(ChiakiTakionAVPacket *packet, ChiakiKeyState *key_state, uint8_t *buf, size_t buf_size); #ifdef __cplusplus } diff --git a/lib/src/ctrl.c b/lib/src/ctrl.c index d4ee356..4fc768c 100644 --- a/lib/src/ctrl.c +++ b/lib/src/ctrl.c @@ -79,7 +79,6 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_ctrl_init(ChiakiCtrl *ctrl, ChiakiSession * goto error_notif_pipe; return err; -error_notif_mutex: chiaki_mutex_fini(&ctrl->notif_mutex); error_notif_pipe: chiaki_stop_pipe_fini(&ctrl->notif_pipe); diff --git a/lib/src/gkcrypt.c b/lib/src/gkcrypt.c index e5eed72..800a98e 100644 --- a/lib/src/gkcrypt.c +++ b/lib/src/gkcrypt.c @@ -210,7 +210,7 @@ CHIAKI_EXPORT void chiaki_gkcrypt_gen_tmp_gmac_key(ChiakiGKCrypt *gkcrypt, uint6 memcpy(key_out, gkcrypt->key_gmac_base, sizeof(gkcrypt->key_gmac_base)); } -CHIAKI_EXPORT ChiakiErrorCode chiaki_gkcrypt_gen_key_stream(ChiakiGKCrypt *gkcrypt, size_t key_pos, uint8_t *buf, size_t buf_size) +CHIAKI_EXPORT ChiakiErrorCode chiaki_gkcrypt_gen_key_stream(ChiakiGKCrypt *gkcrypt, uint64_t key_pos, uint8_t *buf, size_t buf_size) { assert(key_pos % CHIAKI_GKCRYPT_BLOCK_SIZE == 0); assert(buf_size % CHIAKI_GKCRYPT_BLOCK_SIZE == 0); @@ -276,7 +276,7 @@ static bool gkcrypt_key_buf_should_generate(ChiakiGKCrypt *gkcrypt) return gkcrypt->last_key_pos > gkcrypt->key_buf_key_pos_min + gkcrypt->key_buf_populated / 2; } -CHIAKI_EXPORT ChiakiErrorCode chiaki_gkcrypt_get_key_stream(ChiakiGKCrypt *gkcrypt, size_t key_pos, uint8_t *buf, size_t buf_size) +CHIAKI_EXPORT ChiakiErrorCode chiaki_gkcrypt_get_key_stream(ChiakiGKCrypt *gkcrypt, uint64_t key_pos, uint8_t *buf, size_t buf_size) { if(!gkcrypt->key_buf) return chiaki_gkcrypt_gen_key_stream(gkcrypt, key_pos, buf, buf_size); @@ -291,7 +291,15 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_gkcrypt_get_key_stream(ChiakiGKCrypt *gkcry if(key_pos < gkcrypt->key_buf_key_pos_min || key_pos + buf_size >= gkcrypt->key_buf_key_pos_min + gkcrypt->key_buf_populated) { - CHIAKI_LOGW(gkcrypt->log, "Requested key stream for key pos %#llx on GKCrypt %d, but it's not in the buffer", (int)key_pos, gkcrypt->index); + CHIAKI_LOGW(gkcrypt->log, "Requested key stream for key pos %#llx on GKCrypt %d, but it's not in the buffer:" + " key buf size %#llx, start offset: %#llx, populated: %#llx, min key pos: %#llx, last key pos: %#llx", + (unsigned long long)key_pos, + gkcrypt->index, + (unsigned long long)gkcrypt->key_buf_size, + (unsigned long long)gkcrypt->key_buf_start_offset, + (unsigned long long)gkcrypt->key_buf_populated, + (unsigned long long)gkcrypt->key_buf_key_pos_min, + (unsigned long long)gkcrypt->last_key_pos); chiaki_mutex_unlock(&gkcrypt->key_buf_mutex); err = chiaki_gkcrypt_gen_key_stream(gkcrypt, key_pos, buf, buf_size); } @@ -318,9 +326,9 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_gkcrypt_get_key_stream(ChiakiGKCrypt *gkcry return err; } -CHIAKI_EXPORT ChiakiErrorCode chiaki_gkcrypt_decrypt(ChiakiGKCrypt *gkcrypt, size_t key_pos, uint8_t *buf, size_t buf_size) +CHIAKI_EXPORT ChiakiErrorCode chiaki_gkcrypt_decrypt(ChiakiGKCrypt *gkcrypt, uint64_t key_pos, uint8_t *buf, size_t buf_size) { - size_t padding_pre = key_pos % CHIAKI_GKCRYPT_BLOCK_SIZE; + uint64_t padding_pre = key_pos % CHIAKI_GKCRYPT_BLOCK_SIZE; size_t full_size = ((padding_pre + buf_size + CHIAKI_GKCRYPT_BLOCK_SIZE - 1) / CHIAKI_GKCRYPT_BLOCK_SIZE) * CHIAKI_GKCRYPT_BLOCK_SIZE; uint8_t *key_stream = malloc(full_size); @@ -340,7 +348,7 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_gkcrypt_decrypt(ChiakiGKCrypt *gkcrypt, siz return CHIAKI_ERR_SUCCESS; } -CHIAKI_EXPORT ChiakiErrorCode chiaki_gkcrypt_gmac(ChiakiGKCrypt *gkcrypt, size_t key_pos, const uint8_t *buf, size_t buf_size, uint8_t *gmac_out) +CHIAKI_EXPORT ChiakiErrorCode chiaki_gkcrypt_gmac(ChiakiGKCrypt *gkcrypt, uint64_t key_pos, const uint8_t *buf, size_t buf_size, uint8_t *gmac_out) { uint8_t iv[CHIAKI_GKCRYPT_BLOCK_SIZE]; counter_add(iv, gkcrypt->iv, key_pos / 0x10); @@ -463,7 +471,7 @@ static ChiakiErrorCode gkcrypt_generate_next_chunk(ChiakiGKCrypt *gkcrypt) { assert(gkcrypt->key_buf_populated + KEY_BUF_CHUNK_SIZE <= gkcrypt->key_buf_size); size_t buf_offset = (gkcrypt->key_buf_start_offset + gkcrypt->key_buf_populated) % gkcrypt->key_buf_size; - size_t key_pos = gkcrypt->key_buf_key_pos_min + gkcrypt->key_buf_populated; + uint64_t key_pos = gkcrypt->key_buf_key_pos_min + gkcrypt->key_buf_populated; uint8_t *buf_start = gkcrypt->key_buf + buf_offset; chiaki_mutex_unlock(&gkcrypt->key_buf_mutex); @@ -505,7 +513,7 @@ static void *gkcrypt_thread_func(void *user) if(gkcrypt->last_key_pos > gkcrypt->key_buf_key_pos_min + gkcrypt->key_buf_populated) { // skip ahead if the last key pos is already beyond our buffer - size_t key_pos = (gkcrypt->last_key_pos / KEY_BUF_CHUNK_SIZE) * KEY_BUF_CHUNK_SIZE; + uint64_t key_pos = (gkcrypt->last_key_pos / KEY_BUF_CHUNK_SIZE) * KEY_BUF_CHUNK_SIZE; CHIAKI_LOGW(gkcrypt->log, "Already requested a higher key pos than in the buffer, skipping ahead from min %#llx to %#llx", (unsigned long long)gkcrypt->key_buf_key_pos_min, (unsigned long long)key_pos); @@ -533,7 +541,7 @@ CHIAKI_EXPORT void chiaki_key_state_init(ChiakiKeyState *state) state->prev = 0; } -CHIAKI_EXPORT uint64_t chiaki_key_state_request_pos(ChiakiKeyState *state, uint32_t low) +CHIAKI_EXPORT uint64_t chiaki_key_state_request_pos(ChiakiKeyState *state, uint32_t low, bool commit) { uint32_t prev_low = (uint32_t)state->prev; uint32_t high = (uint32_t)(state->prev >> 32); @@ -541,5 +549,13 @@ CHIAKI_EXPORT uint64_t chiaki_key_state_request_pos(ChiakiKeyState *state, uint3 high++; else if(chiaki_seq_num_32_lt(low, prev_low) && low > prev_low && high) high--; - return state->prev = (((uint64_t)high) << 32) | ((uint64_t)low); + uint64_t res = (((uint64_t)high) << 32) | ((uint64_t)low); + if(commit) + state->prev = res; + return res; +} + +CHIAKI_EXPORT void chiaki_key_state_commit(ChiakiKeyState *state, uint64_t prev) +{ + state->prev = prev; } diff --git a/lib/src/senkusha.c b/lib/src/senkusha.c index efa1261..69b8dc4 100644 --- a/lib/src/senkusha.c +++ b/lib/src/senkusha.c @@ -16,6 +16,7 @@ #include #include #include +#include #ifndef _WIN32 #include @@ -85,6 +86,8 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_senkusha_init(ChiakiSenkusha *senkusha, Chi senkusha->ping_tag = 0; senkusha->pong_time_us = 0; + chiaki_key_state_init(&senkusha->takion.key_state); + return CHIAKI_ERR_SUCCESS; error_state_mutex: diff --git a/lib/src/takion.c b/lib/src/takion.c index aef97fd..36374f8 100644 --- a/lib/src/takion.c +++ b/lib/src/takion.c @@ -3,6 +3,7 @@ #include #include #include +#include #include #include @@ -71,6 +72,8 @@ int takion_packet_type_mac_offset(TakionPacketType type) case TAKION_PACKET_TYPE_VIDEO: case TAKION_PACKET_TYPE_AUDIO: return 0xa; + case TAKION_PACKET_TYPE_CONGESTION: + return 7; default: return -1; } @@ -88,6 +91,8 @@ int takion_packet_type_key_pos_offset(TakionPacketType type) case TAKION_PACKET_TYPE_VIDEO: case TAKION_PACKET_TYPE_AUDIO: return 0xe; + case TAKION_PACKET_TYPE_CONGESTION: + return 0xb; default: return -1; } @@ -107,7 +112,7 @@ typedef struct takion_message_t { uint32_t tag; //uint8_t zero[4]; - uint32_t key_pos; + uint64_t key_pos; uint8_t chunk_type; uint8_t chunk_flags; @@ -162,7 +167,7 @@ static void takion_handle_packet_message(ChiakiTakion *takion, uint8_t *buf, siz static void takion_handle_packet_message_data(ChiakiTakion *takion, uint8_t *packet_buf, size_t packet_buf_size, uint8_t type_b, uint8_t *payload, size_t payload_size); static void takion_handle_packet_message_data_ack(ChiakiTakion *takion, uint8_t flags, uint8_t *buf, size_t buf_size); static ChiakiErrorCode takion_parse_message(ChiakiTakion *takion, uint8_t *buf, size_t buf_size, TakionMessage *msg); -static void takion_write_message_header(uint8_t *buf, uint32_t tag, uint32_t key_pos, uint8_t chunk_type, uint8_t chunk_flags, size_t payload_data_size); +static void takion_write_message_header(uint8_t *buf, uint32_t tag, uint64_t key_pos, uint8_t chunk_type, uint8_t chunk_flags, size_t payload_data_size); static ChiakiErrorCode takion_send_message_init(ChiakiTakion *takion, TakionMessagePayloadInit *payload); static ChiakiErrorCode takion_send_message_cookie(ChiakiTakion *takion, uint8_t *cookie); static ChiakiErrorCode takion_recv(ChiakiTakion *takion, uint8_t *buf, size_t *buf_size, uint64_t timeout_ms); @@ -304,7 +309,7 @@ CHIAKI_EXPORT void chiaki_takion_close(ChiakiTakion *takion) chiaki_mutex_fini(&takion->gkcrypt_local_mutex); } -CHIAKI_EXPORT ChiakiErrorCode chiaki_takion_crypt_advance_key_pos(ChiakiTakion *takion, size_t data_size, size_t *key_pos) +CHIAKI_EXPORT ChiakiErrorCode chiaki_takion_crypt_advance_key_pos(ChiakiTakion *takion, size_t data_size, uint64_t *key_pos) { ChiakiErrorCode err = chiaki_mutex_lock(&takion->gkcrypt_local_mutex); if(err != CHIAKI_ERR_SUCCESS) @@ -312,7 +317,7 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_takion_crypt_advance_key_pos(ChiakiTakion * if(takion->gkcrypt_local) { - size_t cur = takion->key_pos_local; + uint64_t cur = takion->key_pos_local; if(SIZE_MAX - cur < data_size) { chiaki_mutex_unlock(&takion->gkcrypt_local_mutex); @@ -337,8 +342,26 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_takion_send_raw(ChiakiTakion *takion, const return CHIAKI_ERR_SUCCESS; } +static ChiakiErrorCode chiaki_takion_packet_read_key_pos(ChiakiTakion *takion, uint8_t *buf, size_t buf_size, uint64_t *key_pos_out) +{ + if(buf_size < 1) + return CHIAKI_ERR_BUF_TOO_SMALL; -static ChiakiErrorCode chiaki_takion_packet_mac(ChiakiGKCrypt *crypt, uint8_t *buf, size_t buf_size, uint8_t *mac_out, uint8_t *mac_old_out, ChiakiTakionPacketKeyPos *key_pos_out) + TakionPacketType base_type = buf[0] & TAKION_PACKET_BASE_TYPE_MASK; + int key_pos_offset = takion_packet_type_key_pos_offset(base_type); + if(key_pos_offset < 0) + return CHIAKI_ERR_INVALID_DATA; + + if(buf_size < key_pos_offset + sizeof(uint32_t)) + return CHIAKI_ERR_BUF_TOO_SMALL; + + uint32_t key_pos_low = ntohl(*((chiaki_unaligned_uint32_t *)(buf + key_pos_offset))); + *key_pos_out = chiaki_key_state_request_pos(&takion->key_state, key_pos_low, false); + + return CHIAKI_ERR_SUCCESS; +} + +static ChiakiErrorCode chiaki_takion_packet_mac(ChiakiGKCrypt *crypt, uint8_t *buf, size_t buf_size, uint64_t key_pos, uint8_t *mac_out, uint8_t *mac_old_out) { if(buf_size < 1) return CHIAKI_ERR_BUF_TOO_SMALL; @@ -349,7 +372,7 @@ static ChiakiErrorCode chiaki_takion_packet_mac(ChiakiGKCrypt *crypt, uint8_t *b if(mac_offset < 0 || key_pos_offset < 0) return CHIAKI_ERR_INVALID_DATA; - if(buf_size < mac_offset + CHIAKI_GKCRYPT_GMAC_SIZE || buf_size < key_pos_offset + sizeof(ChiakiTakionPacketKeyPos)) + if(buf_size < mac_offset + CHIAKI_GKCRYPT_GMAC_SIZE || buf_size < key_pos_offset + sizeof(uint32_t)) return CHIAKI_ERR_BUF_TOO_SMALL; if(mac_old_out) @@ -357,31 +380,31 @@ static ChiakiErrorCode chiaki_takion_packet_mac(ChiakiGKCrypt *crypt, uint8_t *b memset(buf + mac_offset, 0, CHIAKI_GKCRYPT_GMAC_SIZE); - ChiakiTakionPacketKeyPos key_pos = ntohl(*((ChiakiTakionPacketKeyPos *)(buf + key_pos_offset))); - if(crypt) { + uint8_t key_pos_tmp[sizeof(uint32_t)]; if(base_type == TAKION_PACKET_TYPE_CONTROL) - memset(buf + key_pos_offset, 0, sizeof(ChiakiTakionPacketKeyPos)); + { + memcpy(key_pos_tmp, buf + key_pos_offset, sizeof(uint32_t)); + memset(buf + key_pos_offset, 0, sizeof(uint32_t)); + } chiaki_gkcrypt_gmac(crypt, key_pos, buf, buf_size, buf + mac_offset); - *((ChiakiTakionPacketKeyPos *)(buf + key_pos_offset)) = htonl(key_pos); + if(base_type == TAKION_PACKET_TYPE_CONTROL) + memcpy(buf + key_pos_offset, key_pos_tmp, sizeof(uint32_t)); } - if(key_pos_out) - *key_pos_out = key_pos; - memcpy(mac_out, buf + mac_offset, CHIAKI_GKCRYPT_GMAC_SIZE); return CHIAKI_ERR_SUCCESS; } -CHIAKI_EXPORT ChiakiErrorCode chiaki_takion_send(ChiakiTakion *takion, uint8_t *buf, size_t buf_size) +CHIAKI_EXPORT ChiakiErrorCode chiaki_takion_send(ChiakiTakion *takion, uint8_t *buf, size_t buf_size, uint64_t key_pos) { ChiakiErrorCode err = chiaki_mutex_lock(&takion->gkcrypt_local_mutex); if(err != CHIAKI_ERR_SUCCESS) return err; uint8_t mac[CHIAKI_GKCRYPT_GMAC_SIZE]; - err = chiaki_takion_packet_mac(takion->gkcrypt_local, buf, buf_size, mac, NULL, NULL); + err = chiaki_takion_packet_mac(takion->gkcrypt_local, buf, buf_size, key_pos, mac, NULL); chiaki_mutex_unlock(&takion->gkcrypt_local_mutex); if(err != CHIAKI_ERR_SUCCESS) return err; @@ -397,7 +420,7 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_takion_send_message_data(ChiakiTakion *taki // TODO: can we make this more memory-efficient? // TODO: split packet if necessary? - size_t key_pos; + uint64_t key_pos; ChiakiErrorCode err = chiaki_takion_crypt_advance_key_pos(takion, buf_size, &key_pos); if(err != CHIAKI_ERR_SUCCESS) return err; @@ -424,7 +447,7 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_takion_send_message_data(ChiakiTakion *taki *(msg_payload + 8) = 0; memcpy(msg_payload + 9, buf, buf_size); - err = chiaki_takion_send(takion, packet_buf, packet_size); // will alter packet_buf with gmac + err = chiaki_takion_send(takion, packet_buf, packet_size, key_pos); // will alter packet_buf with gmac if(err != CHIAKI_ERR_SUCCESS) { CHIAKI_LOGE(takion->log, "Takion failed to send data packet: %s", chiaki_error_string(err)); @@ -445,7 +468,7 @@ static ChiakiErrorCode chiaki_takion_send_message_data_ack(ChiakiTakion *takion, uint8_t buf[1 + TAKION_MESSAGE_HEADER_SIZE + 0xc]; buf[0] = TAKION_PACKET_TYPE_CONTROL; - size_t key_pos; + uint64_t key_pos; ChiakiErrorCode err = chiaki_takion_crypt_advance_key_pos(takion, sizeof(buf), &key_pos); if(err != CHIAKI_ERR_SUCCESS) return err; @@ -458,7 +481,7 @@ static ChiakiErrorCode chiaki_takion_send_message_data_ack(ChiakiTakion *takion, *((chiaki_unaligned_uint16_t *)(data_ack + 8)) = 0; *((chiaki_unaligned_uint16_t *)(data_ack + 0xa)) = 0; - return chiaki_takion_send(takion, buf, sizeof(buf)); + return chiaki_takion_send(takion, buf, sizeof(buf), key_pos); } CHIAKI_EXPORT ChiakiErrorCode chiaki_takion_send_congestion(ChiakiTakion *takion, ChiakiTakionCongestionPacket *packet) @@ -470,19 +493,15 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_takion_send_congestion(ChiakiTakion *takion *((chiaki_unaligned_uint16_t *)(buf + 3)) = htons(packet->word_1); *((chiaki_unaligned_uint16_t *)(buf + 5)) = htons(packet->word_2); - ChiakiErrorCode err = chiaki_mutex_lock(&takion->gkcrypt_local_mutex); - if(err != CHIAKI_ERR_SUCCESS) - return err; - *((chiaki_unaligned_uint32_t *)(buf + 0xb)) = htonl((uint32_t)takion->key_pos_local); // TODO: is this correct? shouldn't key_pos be 0 for mac calculation? - err = chiaki_gkcrypt_gmac(takion->gkcrypt_local, takion->key_pos_local, buf, sizeof(buf), buf + 7); - takion->key_pos_local += sizeof(buf); - chiaki_mutex_unlock(&takion->gkcrypt_local_mutex); + uint64_t key_pos; + ChiakiErrorCode err = chiaki_takion_crypt_advance_key_pos(takion, sizeof(buf), &key_pos); if(err != CHIAKI_ERR_SUCCESS) return err; + *((chiaki_unaligned_uint32_t *)(buf + 0xb)) = htonl((uint32_t)key_pos); // TODO: is this correct? shouldn't key_pos be 0 for mac calculation? //chiaki_log_hexdump(takion->log, CHIAKI_LOG_DEBUG, buf, sizeof(buf)); - return chiaki_takion_send_raw(takion, buf, sizeof(buf)); + return chiaki_takion_send(takion, buf, sizeof(buf), key_pos); } static ChiakiErrorCode takion_send_feedback_packet(ChiakiTakion *takion, uint8_t *buf, size_t buf_size) @@ -495,7 +514,7 @@ static ChiakiErrorCode takion_send_feedback_packet(ChiakiTakion *takion, uint8_t if(err != CHIAKI_ERR_SUCCESS) return err; - size_t key_pos; + uint64_t key_pos; err = chiaki_takion_crypt_advance_key_pos(takion, payload_size + CHIAKI_GKCRYPT_BLOCK_SIZE, &key_pos); if(err != CHIAKI_ERR_SUCCESS) goto beach; @@ -780,8 +799,14 @@ static ChiakiErrorCode takion_handle_packet_mac(ChiakiTakion *takion, uint8_t ba uint8_t mac[CHIAKI_GKCRYPT_GMAC_SIZE]; uint8_t mac_expected[CHIAKI_GKCRYPT_GMAC_SIZE]; - ChiakiTakionPacketKeyPos key_pos; - ChiakiErrorCode err = chiaki_takion_packet_mac(takion->gkcrypt_remote, buf, buf_size, mac_expected, mac, &key_pos); + uint64_t key_pos; + ChiakiErrorCode err = chiaki_takion_packet_read_key_pos(takion, buf, buf_size, &key_pos); + if(err != CHIAKI_ERR_SUCCESS) + { + CHIAKI_LOGE(takion->log, "Takion failed to pull key_pos out of received packet"); + return err; + } + err = chiaki_takion_packet_mac(takion->gkcrypt_remote, buf, buf_size, key_pos, mac_expected, mac); if(err != CHIAKI_ERR_SUCCESS) { CHIAKI_LOGE(takion->log, "Takion failed to calculate mac for received packet"); @@ -799,6 +824,8 @@ static ChiakiErrorCode takion_handle_packet_mac(ChiakiTakion *takion, uint8_t ba return CHIAKI_ERR_INVALID_MAC; } + chiaki_key_state_commit(&takion->key_state, key_pos); + return CHIAKI_ERR_SUCCESS; } @@ -1014,7 +1041,7 @@ static void takion_handle_packet_message_data_ack(ChiakiTakion *takion, uint8_t * * @param raw_payload_size size of the actual data of the payload excluding type_a, type_b and payload_size */ -static void takion_write_message_header(uint8_t *buf, uint32_t tag, uint32_t key_pos, uint8_t chunk_type, uint8_t chunk_flags, size_t payload_data_size) +static void takion_write_message_header(uint8_t *buf, uint32_t tag, uint64_t key_pos, uint8_t chunk_type, uint8_t chunk_flags, size_t payload_data_size) { *((chiaki_unaligned_uint32_t *)(buf + 0)) = htonl(tag); memset(buf + 4, 0, CHIAKI_GKCRYPT_GMAC_SIZE); @@ -1033,7 +1060,8 @@ static ChiakiErrorCode takion_parse_message(ChiakiTakion *takion, uint8_t *buf, } msg->tag = ntohl(*((chiaki_unaligned_uint32_t *)buf)); - msg->key_pos = ntohl(*((chiaki_unaligned_uint32_t *)(buf + 0x8))); + uint32_t key_pos_low = ntohl(*((chiaki_unaligned_uint32_t *)(buf + 0x8))); + msg->key_pos = chiaki_key_state_request_pos(&takion->key_state, key_pos_low, true); msg->chunk_type = buf[0xc]; msg->chunk_flags = buf[0xd]; msg->payload_size = ntohs(*((chiaki_unaligned_uint16_t *)(buf + 0xe))); @@ -1185,7 +1213,7 @@ static void takion_handle_packet_av(ChiakiTakion *takion, uint8_t base_type, uin assert(base_type == TAKION_PACKET_TYPE_VIDEO || base_type == TAKION_PACKET_TYPE_AUDIO); ChiakiTakionAVPacket packet; - ChiakiErrorCode err = takion->av_packet_parse(&packet, buf, buf_size); + ChiakiErrorCode err = takion->av_packet_parse(&packet, &takion->key_state, buf, buf_size); if(err != CHIAKI_ERR_SUCCESS) { if(err == CHIAKI_ERR_BUF_TOO_SMALL) @@ -1202,7 +1230,7 @@ static void takion_handle_packet_av(ChiakiTakion *takion, uint8_t base_type, uin } } -CHIAKI_EXPORT ChiakiErrorCode chiaki_takion_v9_av_packet_parse(ChiakiTakionAVPacket *packet, uint8_t *buf, size_t buf_size) +CHIAKI_EXPORT ChiakiErrorCode chiaki_takion_v9_av_packet_parse(ChiakiTakionAVPacket *packet, ChiakiKeyState *key_state, uint8_t *buf, size_t buf_size) { memset(packet, 0, sizeof(ChiakiTakionAVPacket)); @@ -1242,8 +1270,8 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_takion_v9_av_packet_parse(ChiakiTakionAVPac } packet->codec = av[8]; - - packet->key_pos = ntohl(*((chiaki_unaligned_uint32_t *)(av + 0xd))); + uint32_t key_pos_low = ntohl(*((chiaki_unaligned_uint32_t *)(av + 0xd))); + packet->key_pos = chiaki_key_state_request_pos(key_state, key_pos_low, true); uint8_t unknown_1 = av[0x11]; @@ -1313,7 +1341,7 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_takion_v7_av_packet_format_header(uint8_t * *(chiaki_unaligned_uint32_t *)(buf + 0xa) = 0; // unknown - *(chiaki_unaligned_uint32_t *)(buf + 0xe) = packet->key_pos; + *(chiaki_unaligned_uint32_t *)(buf + 0xe) = (uint32_t)packet->key_pos; uint8_t *cur = buf + 0x12; if(packet->is_video) @@ -1332,7 +1360,7 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_takion_v7_av_packet_format_header(uint8_t * return CHIAKI_ERR_SUCCESS; } -CHIAKI_EXPORT ChiakiErrorCode chiaki_takion_v7_av_packet_parse(ChiakiTakionAVPacket *packet, uint8_t *buf, size_t buf_size) +CHIAKI_EXPORT ChiakiErrorCode chiaki_takion_v7_av_packet_parse(ChiakiTakionAVPacket *packet, ChiakiKeyState *key_state, uint8_t *buf, size_t buf_size) { memset(packet, 0, sizeof(ChiakiTakionAVPacket)); diff --git a/test/gkcrypt.c b/test/gkcrypt.c index 06ba771..3071aab 100644 --- a/test/gkcrypt.c +++ b/test/gkcrypt.c @@ -58,6 +58,7 @@ static MunitResult test_key_stream(const MunitParameter params[], void *user) static const uint8_t gkcrypt_key[] = { 0x8, 0x81, 0x6f, 0xa2, 0xe5, 0x55, 0x89, 0x61, 0xd5, 0xa2, 0x86, 0xd9, 0xe, 0xec, 0x5b, 0x8c }; static const uint8_t gkcrypt_iv[] = { 0x2a, 0xe1, 0xbb, 0x3d, 0x84, 0xdc, 0x9a, 0xa9, 0xc3, 0x52, 0xa4, 0xcf, 0x3f, 0xfb, 0x8b, 0x72 }; static const uint8_t key_stream[] = { 0xf, 0x6d, 0x89, 0x85, 0x5b, 0xa7, 0x86, 0x74, 0x5b, 0xa1, 0xfe, 0x5c, 0x81, 0x19, 0x6c, 0xd5, 0x54, 0xc4, 0x1c, 0xca, 0xf6, 0xe9, 0x34, 0xa4, 0x89, 0x26, 0x98, 0xb0, 0x62, 0x12, 0xb3, 0x1a }; + static const uint8_t key_stream_high[] = { 0x14, 0x31, 0x9f, 0xf8, 0xbe, 0x08, 0x17, 0xa4, 0xfc, 0x28, 0x49, 0x0b, 0x1e, 0x5f, 0x7b, 0x7e, 0xfa, 0xe9, 0x14, 0xde, 0xc6, 0xdf, 0xe7, 0x5d, 0xd6, 0x9c, 0x75, 0x3f, 0x94, 0x62, 0xd5, 0x2c }; ChiakiLog log; @@ -79,6 +80,17 @@ static MunitResult test_key_stream(const MunitParameter params[], void *user) munit_assert_memory_equal(sizeof(key_stream), key_stream_result, key_stream); + uint64_t key_pos = ((uint64_t)(1ull << 32)) + 0x420; + + err = chiaki_gkcrypt_gen_key_stream(&gkcrypt, key_pos, key_stream_result, sizeof(key_stream_result)); + if (err != CHIAKI_ERR_SUCCESS) + { + chiaki_gkcrypt_fini(&gkcrypt); + return MUNIT_ERROR; + } + + munit_assert_memory_equal(sizeof(key_stream_high), key_stream_result, key_stream_high); + chiaki_gkcrypt_fini(&gkcrypt); return MUNIT_OK; } @@ -92,6 +104,7 @@ static MunitResult test_endecrypt(const MunitParameter params[], void *user) static const uint8_t gkcrypt_iv[] = { 0xef, 0x20, 0x40, 0xc2, 0x15, 0x3c, 0x2, 0x66, 0x32, 0x1f, 0x42, 0xbb, 0xf4, 0x50, 0x34, 0x4d }; static const uint8_t clear_data[] = { 0x4e, 0x61, 0x9f, 0x94, 0x5d, 0x4b, 0x8e, 0xbd, 0x2a, 0x15, 0x4d, 0x3, 0x6a, 0xcd, 0x49, 0x56, 0x9c, 0xc7, 0x5c, 0xe3, 0xe7, 0x0, 0x17, 0x9a, 0x38, 0xd9, 0x69, 0x53, 0x45, 0xf9, 0xc, 0xb5, 0x8c, 0x5, 0x65, 0xf, 0x70 }; static const uint8_t enc_data[] = { 0x23, 0xf4, 0x8d, 0xd8, 0xaa, 0xf9, 0x58, 0x9b, 0xb1, 0x94, 0x4f, 0xad, 0x2b, 0x8d, 0xaa, 0x8d, 0x25, 0x88, 0xfa, 0xf8, 0xb6, 0xd4, 0x17, 0xf4, 0x5f, 0x78, 0xec, 0xf5, 0x4e, 0x37, 0x20, 0xb0, 0x76, 0x81, 0x7, 0x67, 0x9a }; + static const uint8_t enc_data_high[] = { 0x2b, 0x9e, 0xe9, 0x83, 0x27, 0x44, 0x08, 0xb1, 0x17, 0xf5, 0x37, 0xa0, 0xd9, 0xc2, 0xc6, 0x05, 0x95, 0x78, 0xb2, 0x78, 0xfd, 0x17, 0x8c, 0x52, 0xf4, 0x17, 0x9d, 0xee, 0x3f, 0x62, 0xe6, 0x30, 0x01, 0x61, 0x4c, 0xf4, 0xa1 }; ChiakiLog log; @@ -113,6 +126,7 @@ static MunitResult test_endecrypt(const MunitParameter params[], void *user) uint8_t buf[0x25]; + // Low memcpy(buf, clear_data, sizeof(buf)); err = chiaki_gkcrypt_encrypt(&gkcrypt, 0x11, buf, sizeof(buf)); if(err != CHIAKI_ERR_SUCCESS) @@ -131,6 +145,26 @@ static MunitResult test_endecrypt(const MunitParameter params[], void *user) } munit_assert_memory_equal(sizeof(buf), buf, enc_data); + // High + uint64_t key_pos_high = ((uint64_t)(1ull << 32)) + 0x420; + memcpy(buf, clear_data, sizeof(buf)); + err = chiaki_gkcrypt_encrypt(&gkcrypt, key_pos_high, buf, sizeof(buf)); + if (err != CHIAKI_ERR_SUCCESS) + { + chiaki_gkcrypt_fini(&gkcrypt); + return MUNIT_ERROR; + } + munit_assert_memory_equal(sizeof(buf), buf, enc_data_high); + + memcpy(buf, clear_data, sizeof(buf)); + err = chiaki_gkcrypt_decrypt(&gkcrypt, key_pos_high, buf, sizeof(buf)); + if (err != CHIAKI_ERR_SUCCESS) + { + chiaki_gkcrypt_fini(&gkcrypt); + return MUNIT_ERROR; + } + munit_assert_memory_equal(sizeof(buf), buf, enc_data_high); + chiaki_gkcrypt_fini(&gkcrypt); return MUNIT_OK; } @@ -158,15 +192,19 @@ static MunitResult test_gmac(const MunitParameter params[], void *user) 0x64, 0x2d, 0xcb, 0x98, 0x0f, 0x43, 0xa6, 0xe1, 0x4c, 0xe7, 0x9f, 0x2d, 0xab, 0x94, 0x01, 0xd7, 0x52, 0x84, 0x19 }; - static const size_t key_pos = 0x69a0; + static const uint64_t key_pos = 0x69a0; + static const uint64_t key_pos_high = ((uint64_t)(1ull << 32)) + 0x420; static const uint8_t gmac_expected[] = { 0x6f, 0x81, 0x10, 0x97 }; + static const uint8_t gmac_expected_high[] = { 0x4a, 0x30, 0x98, 0x10 }; ChiakiGKCrypt gkcrypt; - memset(&gkcrypt, 0, sizeof(gkcrypt)); + // Low + memset(&gkcrypt, 0, sizeof(gkcrypt)); memcpy(gkcrypt.key_gmac_current, gkcrypt_key, sizeof(gkcrypt.key_gmac_current)); memcpy(gkcrypt.iv, gkcrypt_iv, sizeof(gkcrypt.iv)); + gkcrypt.key_buf = NULL; gkcrypt.key_buf_size = 0; gkcrypt.key_gmac_index_current = 0; @@ -178,6 +216,21 @@ static MunitResult test_gmac(const MunitParameter params[], void *user) munit_assert_memory_equal(sizeof(gmac), gmac, gmac_expected); + // High + memset(&gkcrypt, 0, sizeof(gkcrypt)); + memcpy(gkcrypt.key_gmac_current, gkcrypt_key, sizeof(gkcrypt.key_gmac_current)); + memcpy(gkcrypt.iv, gkcrypt_iv, sizeof(gkcrypt.iv)); + + gkcrypt.key_buf = NULL; + gkcrypt.key_buf_size = 0; + gkcrypt.key_gmac_index_current = 0; + + err = chiaki_gkcrypt_gmac(&gkcrypt, key_pos_high, buf, sizeof(buf), gmac); + if (err != CHIAKI_ERR_SUCCESS) + return MUNIT_ERROR; + + munit_assert_memory_equal(sizeof(gmac), gmac, gmac_expected_high); + return MUNIT_OK; } @@ -242,9 +295,11 @@ static MunitResult test_gmac_split(const MunitParameter params[], void *user) 0xfa, 0x44, 0xc6, 0xd8, 0x19, 0x1, 0x8d, 0x41, 0x1f, 0xc7, 0xf, 0x89, 0x1d, 0xef, 0x34, 0x9a, 0x68, 0x77, 0x8c }; - static const size_t key_pos = 0xadf0; + static const uint64_t key_pos = 0xadf0; + static const uint64_t key_pos_high = ((uint64_t)(1ull << 32)) + 0x420; static const uint8_t gmac_expected[] = { 0xd6, 0x6a, 0x07, 0xb7 }; + static const uint8_t gmac_expected_high[] = { 0xf0, 0x17, 0x95, 0x3c }; ChiakiGKCrypt gkcrypt; memset(&gkcrypt, 0, sizeof(gkcrypt)); @@ -262,6 +317,21 @@ static MunitResult test_gmac_split(const MunitParameter params[], void *user) munit_assert_memory_equal(sizeof(gmac), gmac, gmac_expected); + // High + memset(&gkcrypt, 0, sizeof(gkcrypt)); + + memcpy(gkcrypt.key_gmac_current, gkcrypt_key, sizeof(gkcrypt.key_gmac_current)); + memcpy(gkcrypt.iv, gkcrypt_iv, sizeof(gkcrypt.iv)); + gkcrypt.key_buf = NULL; + gkcrypt.key_buf_size = 0; + gkcrypt.key_gmac_index_current = 0; + + err = chiaki_gkcrypt_gmac(&gkcrypt, key_pos_high, buf, sizeof(buf), gmac); + if (err != CHIAKI_ERR_SUCCESS) + return MUNIT_ERROR; + + munit_assert_memory_equal(sizeof(gmac), gmac, gmac_expected_high); + return MUNIT_OK; } @@ -290,7 +360,7 @@ static MunitResult test_gmac_multiple_of_key_refresh(const MunitParameter params 0xbe, 0xfd, 0xee }; static const uint8_t crypt_index = 3; - static const size_t key_pos = 0x6b1de0; // % CHIAKI_GKCRYPT_GMAC_KEY_REFRESH_KEY_POS == 0 + static const uint64_t key_pos = 0x6b1de0; // % CHIAKI_GKCRYPT_GMAC_KEY_REFRESH_KEY_POS == 0 static const uint8_t gmac_expected[] = { 0x20, 0xcc, 0xa5, 0xf1 }; ChiakiLog log; diff --git a/test/keystate.c b/test/keystate.c index f603bb8..74ca77a 100644 --- a/test/keystate.c +++ b/test/keystate.c @@ -9,26 +9,51 @@ static MunitResult test_key_state(const MunitParameter params[], void *user) ChiakiKeyState state; chiaki_key_state_init(&state); - uint64_t pos = chiaki_key_state_request_pos(&state, 0); + uint64_t pos = chiaki_key_state_request_pos(&state, 0, true); munit_assert_uint64(pos, ==, 0); - pos = chiaki_key_state_request_pos(&state, 0x1337); + pos = chiaki_key_state_request_pos(&state, 0x1337, true); munit_assert_uint64(pos, ==, 0x1337); - pos = chiaki_key_state_request_pos(&state, 0xffff0000); + pos = chiaki_key_state_request_pos(&state, 0xffff0000, true); munit_assert_uint64(pos, ==, 0xffff0000); - pos = chiaki_key_state_request_pos(&state, 0x1337); + pos = chiaki_key_state_request_pos(&state, 0x1337, true); munit_assert_uint64(pos, ==, 0x100001337); - pos = chiaki_key_state_request_pos(&state, 0xffff1337); + pos = chiaki_key_state_request_pos(&state, 0xffff1337, true); munit_assert_uint64(pos, ==, 0xffff1337); - pos = chiaki_key_state_request_pos(&state, 0x50000000); + pos = chiaki_key_state_request_pos(&state, 0x50000000, true); munit_assert_uint64(pos, ==, 0x150000000); - pos = chiaki_key_state_request_pos(&state, 0xb0000000); + pos = chiaki_key_state_request_pos(&state, 0xb0000000, true); munit_assert_uint64(pos, ==, 0x1b0000000); - pos = chiaki_key_state_request_pos(&state, 0x00000000); + pos = chiaki_key_state_request_pos(&state, 0x00000000, true); munit_assert_uint64(pos, ==, 0x200000000); return MUNIT_OK; } +static MunitResult test_key_state_nocommit(const MunitParameter params[], void *user) +{ + ChiakiKeyState state; + chiaki_key_state_init(&state); + + uint64_t pos = chiaki_key_state_request_pos(&state, 0, false); + munit_assert_uint64(pos, ==, 0); + pos = chiaki_key_state_request_pos(&state, 0x1337, false); + munit_assert_uint64(pos, ==, 0x1337); + pos = chiaki_key_state_request_pos(&state, 0xffff0000, false); + munit_assert_uint64(pos, ==, 0xffff0000); + pos = chiaki_key_state_request_pos(&state, 0x1337, false); + munit_assert_uint64(pos, ==, 0x1337); + pos = chiaki_key_state_request_pos(&state, 0xffff1337, false); + munit_assert_uint64(pos, ==, 0xffff1337); + pos = chiaki_key_state_request_pos(&state, 0x50000000, false); + munit_assert_uint64(pos, ==, 0x50000000); + pos = chiaki_key_state_request_pos(&state, 0xb0000000, false); + munit_assert_uint64(pos, ==, 0xb0000000); + pos = chiaki_key_state_request_pos(&state, 0x00000000, false); + munit_assert_uint64(pos, ==, 0); + + return MUNIT_OK; +} + MunitTest tests_key_state[] = { { "/key_state", @@ -38,5 +63,13 @@ MunitTest tests_key_state[] = { MUNIT_TEST_OPTION_NONE, NULL }, + { + "/key_state_nocommit", + test_key_state_nocommit, + NULL, + NULL, + MUNIT_TEST_OPTION_NONE, + NULL + }, { NULL, NULL, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL } }; diff --git a/test/takion.c b/test/takion.c index 8e592d0..6bf342f 100644 --- a/test/takion.c +++ b/test/takion.c @@ -28,9 +28,12 @@ static MunitResult test_av_packet_parse(const MunitParameter params[], void *use 0x57, 0x5a, 0x76, 0x0, 0xc5, 0xe0, 0x93, 0xa9, 0xf5, 0x32, 0x5d, 0xee, 0xf7, 0x9d }; + ChiakiKeyState key_state; + chiaki_key_state_init(&key_state); + ChiakiTakionAVPacket av_packet; - ChiakiErrorCode err = chiaki_takion_v9_av_packet_parse(&av_packet, packet, sizeof(packet)); + ChiakiErrorCode err = chiaki_takion_v9_av_packet_parse(&av_packet, &key_state, packet, sizeof(packet)); munit_assert_int(err, ==, CHIAKI_ERR_SUCCESS); munit_assert(av_packet.is_video); diff --git a/test/takion_av_packet_parse_real_video.inl b/test/takion_av_packet_parse_real_video.inl index b57b787..b5b7004 100644 --- a/test/takion_av_packet_parse_real_video.inl +++ b/test/takion_av_packet_parse_real_video.inl @@ -9,7 +9,9 @@ if(err != CHIAKI_ERR_SUCCESS) return MUNIT_ERROR; -// -- frame -- +// -- frame -- ChiakiKeyState key_state; +ChiakiKeyState key_state; +chiaki_key_state_init(&key_state); uint8_t packet_0[126]; size_t packet_0_size = sizeof(packet_0); if(chiaki_base64_decode("AgAAAAEAAAQBAwlCI7oAAACAA5gAhJT79yG83L9WhXCDP/h48VDjBL03cGscKH6DLQ3ZOzh007JcllEEIpTP3DbJwYAF5MGH2BwWJJTcbZATOGrzCznFMInIL7pWssSgxQBcf2q+u+vECFWQ5zvEQsq4RxQs3bre0DchyEMz", 168, packet_0, &packet_0_size) != CHIAKI_ERR_SUCCESS || packet_0_size != 126) return MUNIT_ERROR; uint8_t nalu_0[105]; size_t nalu_0_size = sizeof(nalu_0); if(chiaki_base64_decode("AAMAAAABZYiAhn8AL8gD/wh+/miASsv/rw5fm2PYQAAAAwAAAwAFIc42jXIHLchAAAe8A/i2oRmALiO8cB3sEzDosBxyZyFg4KK5rAXoBy0/2EfQAAADAAADAAADAAADAAADAAADAAJq", 140, nalu_0, &nalu_0_size) != CHIAKI_ERR_SUCCESS || nalu_0_size != 105) return MUNIT_ERROR; @@ -17,7 +19,7 @@ uint8_t nalu_0[105]; size_t nalu_0_size = sizeof(nalu_0); if(chiaki_base64_decod ChiakiTakionAVPacket av_packet_0; memset(&av_packet_0, 0, sizeof(av_packet_0)); -chiaki_takion_v9_av_packet_parse(&av_packet_0, packet_0, sizeof(packet_0)); +chiaki_takion_v9_av_packet_parse(&av_packet_0, &key_state, packet_0, sizeof(packet_0)); munit_assert(av_packet_0.is_video); munit_assert_size(av_packet_0.data_size, ==, sizeof(nalu_0)); chiaki_gkcrypt_decrypt(&gkcrypt, av_packet_0.key_pos + CHIAKI_GKCRYPT_BLOCK_SIZE, av_packet_0.data, av_packet_0.data_size); @@ -30,7 +32,7 @@ uint8_t nalu_1[108]; size_t nalu_1_size = sizeof(nalu_1); if(chiaki_base64_decod ChiakiTakionAVPacket av_packet_1; memset(&av_packet_1, 0, sizeof(av_packet_1)); -chiaki_takion_v9_av_packet_parse(&av_packet_1, packet_1, sizeof(packet_1)); +chiaki_takion_v9_av_packet_parse(&av_packet_1, &key_state, packet_1, sizeof(packet_1)); munit_assert(av_packet_1.is_video); munit_assert_size(av_packet_1.data_size, ==, sizeof(nalu_1)); chiaki_gkcrypt_decrypt(&gkcrypt, av_packet_1.key_pos + CHIAKI_GKCRYPT_BLOCK_SIZE, av_packet_1.data, av_packet_1.data_size); @@ -43,7 +45,7 @@ uint8_t nalu_2[1400]; size_t nalu_2_size = sizeof(nalu_2); if(chiaki_base64_deco ChiakiTakionAVPacket av_packet_2; memset(&av_packet_2, 0, sizeof(av_packet_2)); -chiaki_takion_v9_av_packet_parse(&av_packet_2, packet_2, sizeof(packet_2)); +chiaki_takion_v9_av_packet_parse(&av_packet_2, &key_state, packet_2, sizeof(packet_2)); munit_assert(av_packet_2.is_video); munit_assert_size(av_packet_2.data_size, ==, sizeof(nalu_2)); chiaki_gkcrypt_decrypt(&gkcrypt, av_packet_2.key_pos + CHIAKI_GKCRYPT_BLOCK_SIZE, av_packet_2.data, av_packet_2.data_size); @@ -56,7 +58,7 @@ uint8_t nalu_3[12]; size_t nalu_3_size = sizeof(nalu_3); if(chiaki_base64_decode ChiakiTakionAVPacket av_packet_3; memset(&av_packet_3, 0, sizeof(av_packet_3)); -chiaki_takion_v9_av_packet_parse(&av_packet_3, packet_3, sizeof(packet_3)); +chiaki_takion_v9_av_packet_parse(&av_packet_3, &key_state, packet_3, sizeof(packet_3)); munit_assert(av_packet_3.is_video); munit_assert_size(av_packet_3.data_size, ==, sizeof(nalu_3)); chiaki_gkcrypt_decrypt(&gkcrypt, av_packet_3.key_pos + CHIAKI_GKCRYPT_BLOCK_SIZE, av_packet_3.data, av_packet_3.data_size); @@ -69,7 +71,7 @@ uint8_t nalu_4[1400]; size_t nalu_4_size = sizeof(nalu_4); if(chiaki_base64_deco ChiakiTakionAVPacket av_packet_4; memset(&av_packet_4, 0, sizeof(av_packet_4)); -chiaki_takion_v9_av_packet_parse(&av_packet_4, packet_4, sizeof(packet_4)); +chiaki_takion_v9_av_packet_parse(&av_packet_4, &key_state, packet_4, sizeof(packet_4)); munit_assert(av_packet_4.is_video); munit_assert_size(av_packet_4.data_size, ==, sizeof(nalu_4)); chiaki_gkcrypt_decrypt(&gkcrypt, av_packet_4.key_pos + CHIAKI_GKCRYPT_BLOCK_SIZE, av_packet_4.data, av_packet_4.data_size); @@ -82,7 +84,7 @@ uint8_t nalu_5[139]; size_t nalu_5_size = sizeof(nalu_5); if(chiaki_base64_decod ChiakiTakionAVPacket av_packet_5; memset(&av_packet_5, 0, sizeof(av_packet_5)); -chiaki_takion_v9_av_packet_parse(&av_packet_5, packet_5, sizeof(packet_5)); +chiaki_takion_v9_av_packet_parse(&av_packet_5, &key_state, packet_5, sizeof(packet_5)); munit_assert(av_packet_5.is_video); munit_assert_size(av_packet_5.data_size, ==, sizeof(nalu_5)); chiaki_gkcrypt_decrypt(&gkcrypt, av_packet_5.key_pos + CHIAKI_GKCRYPT_BLOCK_SIZE, av_packet_5.data, av_packet_5.data_size); @@ -95,7 +97,7 @@ uint8_t nalu_6[1339]; size_t nalu_6_size = sizeof(nalu_6); if(chiaki_base64_deco ChiakiTakionAVPacket av_packet_6; memset(&av_packet_6, 0, sizeof(av_packet_6)); -chiaki_takion_v9_av_packet_parse(&av_packet_6, packet_6, sizeof(packet_6)); +chiaki_takion_v9_av_packet_parse(&av_packet_6, &key_state, packet_6, sizeof(packet_6)); munit_assert(av_packet_6.is_video); munit_assert_size(av_packet_6.data_size, ==, sizeof(nalu_6)); chiaki_gkcrypt_decrypt(&gkcrypt, av_packet_6.key_pos + CHIAKI_GKCRYPT_BLOCK_SIZE, av_packet_6.data, av_packet_6.data_size); @@ -108,7 +110,7 @@ uint8_t nalu_7[1400]; size_t nalu_7_size = sizeof(nalu_7); if(chiaki_base64_deco ChiakiTakionAVPacket av_packet_7; memset(&av_packet_7, 0, sizeof(av_packet_7)); -chiaki_takion_v9_av_packet_parse(&av_packet_7, packet_7, sizeof(packet_7)); +chiaki_takion_v9_av_packet_parse(&av_packet_7, &key_state, packet_7, sizeof(packet_7)); munit_assert(av_packet_7.is_video); munit_assert_size(av_packet_7.data_size, ==, sizeof(nalu_7)); chiaki_gkcrypt_decrypt(&gkcrypt, av_packet_7.key_pos + CHIAKI_GKCRYPT_BLOCK_SIZE, av_packet_7.data, av_packet_7.data_size); @@ -121,7 +123,7 @@ uint8_t nalu_8[172]; size_t nalu_8_size = sizeof(nalu_8); if(chiaki_base64_decod ChiakiTakionAVPacket av_packet_8; memset(&av_packet_8, 0, sizeof(av_packet_8)); -chiaki_takion_v9_av_packet_parse(&av_packet_8, packet_8, sizeof(packet_8)); +chiaki_takion_v9_av_packet_parse(&av_packet_8, &key_state, packet_8, sizeof(packet_8)); munit_assert(av_packet_8.is_video); munit_assert_size(av_packet_8.data_size, ==, sizeof(nalu_8)); chiaki_gkcrypt_decrypt(&gkcrypt, av_packet_8.key_pos + CHIAKI_GKCRYPT_BLOCK_SIZE, av_packet_8.data, av_packet_8.data_size); @@ -134,7 +136,7 @@ uint8_t nalu_9[1351]; size_t nalu_9_size = sizeof(nalu_9); if(chiaki_base64_deco ChiakiTakionAVPacket av_packet_9; memset(&av_packet_9, 0, sizeof(av_packet_9)); -chiaki_takion_v9_av_packet_parse(&av_packet_9, packet_9, sizeof(packet_9)); +chiaki_takion_v9_av_packet_parse(&av_packet_9, &key_state, packet_9, sizeof(packet_9)); munit_assert(av_packet_9.is_video); munit_assert_size(av_packet_9.data_size, ==, sizeof(nalu_9)); chiaki_gkcrypt_decrypt(&gkcrypt, av_packet_9.key_pos + CHIAKI_GKCRYPT_BLOCK_SIZE, av_packet_9.data, av_packet_9.data_size); @@ -147,7 +149,7 @@ uint8_t nalu_10[1400]; size_t nalu_10_size = sizeof(nalu_10); if(chiaki_base64_d ChiakiTakionAVPacket av_packet_10; memset(&av_packet_10, 0, sizeof(av_packet_10)); -chiaki_takion_v9_av_packet_parse(&av_packet_10, packet_10, sizeof(packet_10)); +chiaki_takion_v9_av_packet_parse(&av_packet_10, &key_state, packet_10, sizeof(packet_10)); munit_assert(av_packet_10.is_video); munit_assert_size(av_packet_10.data_size, ==, sizeof(nalu_10)); chiaki_gkcrypt_decrypt(&gkcrypt, av_packet_10.key_pos + CHIAKI_GKCRYPT_BLOCK_SIZE, av_packet_10.data, av_packet_10.data_size); @@ -160,7 +162,7 @@ uint8_t nalu_11[11]; size_t nalu_11_size = sizeof(nalu_11); if(chiaki_base64_dec ChiakiTakionAVPacket av_packet_11; memset(&av_packet_11, 0, sizeof(av_packet_11)); -chiaki_takion_v9_av_packet_parse(&av_packet_11, packet_11, sizeof(packet_11)); +chiaki_takion_v9_av_packet_parse(&av_packet_11, &key_state, packet_11, sizeof(packet_11)); munit_assert(av_packet_11.is_video); munit_assert_size(av_packet_11.data_size, ==, sizeof(nalu_11)); chiaki_gkcrypt_decrypt(&gkcrypt, av_packet_11.key_pos + CHIAKI_GKCRYPT_BLOCK_SIZE, av_packet_11.data, av_packet_11.data_size); @@ -173,7 +175,7 @@ uint8_t nalu_12[1400]; size_t nalu_12_size = sizeof(nalu_12); if(chiaki_base64_d ChiakiTakionAVPacket av_packet_12; memset(&av_packet_12, 0, sizeof(av_packet_12)); -chiaki_takion_v9_av_packet_parse(&av_packet_12, packet_12, sizeof(packet_12)); +chiaki_takion_v9_av_packet_parse(&av_packet_12, &key_state, packet_12, sizeof(packet_12)); munit_assert(av_packet_12.is_video); munit_assert_size(av_packet_12.data_size, ==, sizeof(nalu_12)); chiaki_gkcrypt_decrypt(&gkcrypt, av_packet_12.key_pos + CHIAKI_GKCRYPT_BLOCK_SIZE, av_packet_12.data, av_packet_12.data_size); @@ -186,7 +188,7 @@ uint8_t nalu_13[10]; size_t nalu_13_size = sizeof(nalu_13); if(chiaki_base64_dec ChiakiTakionAVPacket av_packet_13; memset(&av_packet_13, 0, sizeof(av_packet_13)); -chiaki_takion_v9_av_packet_parse(&av_packet_13, packet_13, sizeof(packet_13)); +chiaki_takion_v9_av_packet_parse(&av_packet_13, &key_state, packet_13, sizeof(packet_13)); munit_assert(av_packet_13.is_video); munit_assert_size(av_packet_13.data_size, ==, sizeof(nalu_13)); chiaki_gkcrypt_decrypt(&gkcrypt, av_packet_13.key_pos + CHIAKI_GKCRYPT_BLOCK_SIZE, av_packet_13.data, av_packet_13.data_size); @@ -199,7 +201,7 @@ uint8_t nalu_14[1378]; size_t nalu_14_size = sizeof(nalu_14); if(chiaki_base64_d ChiakiTakionAVPacket av_packet_14; memset(&av_packet_14, 0, sizeof(av_packet_14)); -chiaki_takion_v9_av_packet_parse(&av_packet_14, packet_14, sizeof(packet_14)); +chiaki_takion_v9_av_packet_parse(&av_packet_14, &key_state, packet_14, sizeof(packet_14)); munit_assert(av_packet_14.is_video); munit_assert_size(av_packet_14.data_size, ==, sizeof(nalu_14)); chiaki_gkcrypt_decrypt(&gkcrypt, av_packet_14.key_pos + CHIAKI_GKCRYPT_BLOCK_SIZE, av_packet_14.data, av_packet_14.data_size); @@ -212,7 +214,7 @@ uint8_t nalu_15[1353]; size_t nalu_15_size = sizeof(nalu_15); if(chiaki_base64_d ChiakiTakionAVPacket av_packet_15; memset(&av_packet_15, 0, sizeof(av_packet_15)); -chiaki_takion_v9_av_packet_parse(&av_packet_15, packet_15, sizeof(packet_15)); +chiaki_takion_v9_av_packet_parse(&av_packet_15, &key_state, packet_15, sizeof(packet_15)); munit_assert(av_packet_15.is_video); munit_assert_size(av_packet_15.data_size, ==, sizeof(nalu_15)); chiaki_gkcrypt_decrypt(&gkcrypt, av_packet_15.key_pos + CHIAKI_GKCRYPT_BLOCK_SIZE, av_packet_15.data, av_packet_15.data_size); @@ -225,7 +227,7 @@ uint8_t nalu_16[1332]; size_t nalu_16_size = sizeof(nalu_16); if(chiaki_base64_d ChiakiTakionAVPacket av_packet_16; memset(&av_packet_16, 0, sizeof(av_packet_16)); -chiaki_takion_v9_av_packet_parse(&av_packet_16, packet_16, sizeof(packet_16)); +chiaki_takion_v9_av_packet_parse(&av_packet_16, &key_state, packet_16, sizeof(packet_16)); munit_assert(av_packet_16.is_video); munit_assert_size(av_packet_16.data_size, ==, sizeof(nalu_16)); chiaki_gkcrypt_decrypt(&gkcrypt, av_packet_16.key_pos + CHIAKI_GKCRYPT_BLOCK_SIZE, av_packet_16.data, av_packet_16.data_size); @@ -238,7 +240,7 @@ uint8_t nalu_17[1357]; size_t nalu_17_size = sizeof(nalu_17); if(chiaki_base64_d ChiakiTakionAVPacket av_packet_17; memset(&av_packet_17, 0, sizeof(av_packet_17)); -chiaki_takion_v9_av_packet_parse(&av_packet_17, packet_17, sizeof(packet_17)); +chiaki_takion_v9_av_packet_parse(&av_packet_17, &key_state, packet_17, sizeof(packet_17)); munit_assert(av_packet_17.is_video); munit_assert_size(av_packet_17.data_size, ==, sizeof(nalu_17)); chiaki_gkcrypt_decrypt(&gkcrypt, av_packet_17.key_pos + CHIAKI_GKCRYPT_BLOCK_SIZE, av_packet_17.data, av_packet_17.data_size); @@ -254,7 +256,7 @@ uint8_t nalu_18[575]; size_t nalu_18_size = sizeof(nalu_18); if(chiaki_base64_de ChiakiTakionAVPacket av_packet_18; memset(&av_packet_18, 0, sizeof(av_packet_18)); -chiaki_takion_v9_av_packet_parse(&av_packet_18, packet_18, sizeof(packet_18)); +chiaki_takion_v9_av_packet_parse(&av_packet_18, &key_state, packet_18, sizeof(packet_18)); munit_assert(av_packet_18.is_video); munit_assert_size(av_packet_18.data_size, ==, sizeof(nalu_18)); chiaki_gkcrypt_decrypt(&gkcrypt, av_packet_18.key_pos + CHIAKI_GKCRYPT_BLOCK_SIZE, av_packet_18.data, av_packet_18.data_size); @@ -267,7 +269,7 @@ uint8_t nalu_19[1400]; size_t nalu_19_size = sizeof(nalu_19); if(chiaki_base64_d ChiakiTakionAVPacket av_packet_19; memset(&av_packet_19, 0, sizeof(av_packet_19)); -chiaki_takion_v9_av_packet_parse(&av_packet_19, packet_19, sizeof(packet_19)); +chiaki_takion_v9_av_packet_parse(&av_packet_19, &key_state, packet_19, sizeof(packet_19)); munit_assert(av_packet_19.is_video); munit_assert_size(av_packet_19.data_size, ==, sizeof(nalu_19)); chiaki_gkcrypt_decrypt(&gkcrypt, av_packet_19.key_pos + CHIAKI_GKCRYPT_BLOCK_SIZE, av_packet_19.data, av_packet_19.data_size); @@ -280,7 +282,7 @@ uint8_t nalu_20[1328]; size_t nalu_20_size = sizeof(nalu_20); if(chiaki_base64_d ChiakiTakionAVPacket av_packet_20; memset(&av_packet_20, 0, sizeof(av_packet_20)); -chiaki_takion_v9_av_packet_parse(&av_packet_20, packet_20, sizeof(packet_20)); +chiaki_takion_v9_av_packet_parse(&av_packet_20, &key_state, packet_20, sizeof(packet_20)); munit_assert(av_packet_20.is_video); munit_assert_size(av_packet_20.data_size, ==, sizeof(nalu_20)); chiaki_gkcrypt_decrypt(&gkcrypt, av_packet_20.key_pos + CHIAKI_GKCRYPT_BLOCK_SIZE, av_packet_20.data, av_packet_20.data_size); @@ -293,7 +295,7 @@ uint8_t nalu_21[1313]; size_t nalu_21_size = sizeof(nalu_21); if(chiaki_base64_d ChiakiTakionAVPacket av_packet_21; memset(&av_packet_21, 0, sizeof(av_packet_21)); -chiaki_takion_v9_av_packet_parse(&av_packet_21, packet_21, sizeof(packet_21)); +chiaki_takion_v9_av_packet_parse(&av_packet_21, &key_state, packet_21, sizeof(packet_21)); munit_assert(av_packet_21.is_video); munit_assert_size(av_packet_21.data_size, ==, sizeof(nalu_21)); chiaki_gkcrypt_decrypt(&gkcrypt, av_packet_21.key_pos + CHIAKI_GKCRYPT_BLOCK_SIZE, av_packet_21.data, av_packet_21.data_size); @@ -306,7 +308,7 @@ uint8_t nalu_22[1331]; size_t nalu_22_size = sizeof(nalu_22); if(chiaki_base64_d ChiakiTakionAVPacket av_packet_22; memset(&av_packet_22, 0, sizeof(av_packet_22)); -chiaki_takion_v9_av_packet_parse(&av_packet_22, packet_22, sizeof(packet_22)); +chiaki_takion_v9_av_packet_parse(&av_packet_22, &key_state, packet_22, sizeof(packet_22)); munit_assert(av_packet_22.is_video); munit_assert_size(av_packet_22.data_size, ==, sizeof(nalu_22)); chiaki_gkcrypt_decrypt(&gkcrypt, av_packet_22.key_pos + CHIAKI_GKCRYPT_BLOCK_SIZE, av_packet_22.data, av_packet_22.data_size); @@ -319,7 +321,7 @@ uint8_t nalu_23[1307]; size_t nalu_23_size = sizeof(nalu_23); if(chiaki_base64_d ChiakiTakionAVPacket av_packet_23; memset(&av_packet_23, 0, sizeof(av_packet_23)); -chiaki_takion_v9_av_packet_parse(&av_packet_23, packet_23, sizeof(packet_23)); +chiaki_takion_v9_av_packet_parse(&av_packet_23, &key_state, packet_23, sizeof(packet_23)); munit_assert(av_packet_23.is_video); munit_assert_size(av_packet_23.data_size, ==, sizeof(nalu_23)); chiaki_gkcrypt_decrypt(&gkcrypt, av_packet_23.key_pos + CHIAKI_GKCRYPT_BLOCK_SIZE, av_packet_23.data, av_packet_23.data_size); From 8fbd1b942714e86c5173142080642d342c7bbfa3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Wed, 4 Nov 2020 12:09:42 +0100 Subject: [PATCH 064/237] Warn harder and fix Warnings --- lib/CMakeLists.txt | 4 ++++ lib/src/base64.c | 4 ++-- lib/src/ctrl.c | 1 + lib/src/pb_utils.h | 8 ++++---- lib/src/session.c | 8 -------- lib/src/takion.c | 2 +- 6 files changed, 12 insertions(+), 15 deletions(-) diff --git a/lib/CMakeLists.txt b/lib/CMakeLists.txt index a564577..4d9e98f 100644 --- a/lib/CMakeLists.txt +++ b/lib/CMakeLists.txt @@ -86,6 +86,10 @@ add_library(chiaki-lib ${HEADER_FILES} ${SOURCE_FILES} ${CHIAKI_LIB_PROTO_SOURCE configure_file(config.h.in include/chiaki/config.h) target_include_directories(chiaki-lib PUBLIC "${CMAKE_CURRENT_BINARY_DIR}/include") +if(CMAKE_C_COMPILER_ID STREQUAL GNU OR CMAKE_C_COMPILER_ID STREQUAL Clang) + target_compile_options(chiaki-lib PRIVATE -Wall) +endif() + add_dependencies(chiaki-lib chiaki-pb) set_target_properties(chiaki-lib PROPERTIES OUTPUT_NAME chiaki) diff --git a/lib/src/base64.c b/lib/src/base64.c index 297eb3e..5882ff8 100644 --- a/lib/src/base64.c +++ b/lib/src/base64.c @@ -110,7 +110,7 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_base64_decode(const char *in, size_t in_siz while (in < end) { - unsigned char c = d[*in++]; + unsigned char c = d[(size_t)(*in++)]; switch(c) { @@ -153,4 +153,4 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_base64_decode(const char *in, size_t in_siz *out_size = len; return CHIAKI_ERR_SUCCESS; -} \ No newline at end of file +} diff --git a/lib/src/ctrl.c b/lib/src/ctrl.c index 4fc768c..b410741 100644 --- a/lib/src/ctrl.c +++ b/lib/src/ctrl.c @@ -157,6 +157,7 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_ctrl_send_message(ChiakiCtrl *ctrl, uint16_ } chiaki_mutex_unlock(&ctrl->notif_mutex); chiaki_stop_pipe_stop(&ctrl->notif_pipe); + return CHIAKI_ERR_SUCCESS; } CHIAKI_EXPORT void chiaki_ctrl_set_login_pin(ChiakiCtrl *ctrl, const uint8_t *pin, size_t pin_size) diff --git a/lib/src/pb_utils.h b/lib/src/pb_utils.h index d01b204..70ecd5a 100644 --- a/lib/src/pb_utils.h +++ b/lib/src/pb_utils.h @@ -6,7 +6,7 @@ #include #include -static bool chiaki_pb_encode_string(pb_ostream_t *stream, const pb_field_t *field, void *const *arg) +static inline bool chiaki_pb_encode_string(pb_ostream_t *stream, const pb_field_t *field, void *const *arg) { char *str = *arg; @@ -22,7 +22,7 @@ typedef struct chiaki_pb_buf_t uint8_t *buf; } ChiakiPBBuf; -static bool chiaki_pb_encode_buf(pb_ostream_t *stream, const pb_field_t *field, void *const *arg) +static inline bool chiaki_pb_encode_buf(pb_ostream_t *stream, const pb_field_t *field, void *const *arg) { ChiakiPBBuf *buf = *arg; @@ -40,7 +40,7 @@ typedef struct chiaki_pb_decode_buf_t uint8_t *buf; } ChiakiPBDecodeBuf; -static bool chiaki_pb_decode_buf(pb_istream_t *stream, const pb_field_t *field, void **arg) +static inline bool chiaki_pb_decode_buf(pb_istream_t *stream, const pb_field_t *field, void **arg) { ChiakiPBDecodeBuf *buf = *arg; if(stream->bytes_left > buf->max_size) @@ -63,7 +63,7 @@ typedef struct chiaki_pb_decode_buf_alloc_t uint8_t *buf; } ChiakiPBDecodeBufAlloc; -static bool chiaki_pb_decode_buf_alloc(pb_istream_t *stream, const pb_field_t *field, void **arg) +static inline bool chiaki_pb_decode_buf_alloc(pb_istream_t *stream, const pb_field_t *field, void **arg) { ChiakiPBDecodeBufAlloc *buf = *arg; buf->size = stream->bytes_left; diff --git a/lib/src/session.c b/lib/src/session.c index c0850d8..fd1411d 100644 --- a/lib/src/session.c +++ b/lib/src/session.c @@ -334,14 +334,6 @@ static bool session_check_state_pred_pin(void *user) || session->login_pin_entered; } -static bool session_check_state_pred_session_id(void *user) -{ - ChiakiSession *session = user; - return session->should_stop - || session->ctrl_failed - || session->ctrl_session_id_received; -} - #define ENABLE_SENKUSHA static void *session_thread_func(void *arg) diff --git a/lib/src/takion.c b/lib/src/takion.c index 36374f8..dcb26f2 100644 --- a/lib/src/takion.c +++ b/lib/src/takion.c @@ -1273,7 +1273,7 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_takion_v9_av_packet_parse(ChiakiTakionAVPac uint32_t key_pos_low = ntohl(*((chiaki_unaligned_uint32_t *)(av + 0xd))); packet->key_pos = chiaki_key_state_request_pos(key_state, key_pos_low, true); - uint8_t unknown_1 = av[0x11]; + uint8_t unknown_1 = av[0x11]; (void)unknown_1; av += 0x11; av_size -= 0x11; From 4ed2e4d6a9073432d816a609a743ef694b20f596 Mon Sep 17 00:00:00 2001 From: Sven Scharmentke Date: Sat, 7 Nov 2020 11:14:16 +0100 Subject: [PATCH 065/237] Add Text Input Support to Library (#361) --- lib/include/chiaki/ctrl.h | 4 + lib/include/chiaki/session.h | 14 ++- lib/src/ctrl.c | 167 ++++++++++++++++++++++++++++++++++- lib/src/session.c | 15 ++++ 4 files changed, 197 insertions(+), 3 deletions(-) diff --git a/lib/include/chiaki/ctrl.h b/lib/include/chiaki/ctrl.h index 71671ec..c219b3d 100644 --- a/lib/include/chiaki/ctrl.h +++ b/lib/include/chiaki/ctrl.h @@ -45,6 +45,7 @@ typedef struct chiaki_ctrl_t size_t recv_buf_size; uint64_t crypt_counter_local; uint64_t crypt_counter_remote; + uint32_t keyboard_text_counter; } ChiakiCtrl; CHIAKI_EXPORT ChiakiErrorCode chiaki_ctrl_init(ChiakiCtrl *ctrl, struct chiaki_session_t *session); @@ -55,6 +56,9 @@ CHIAKI_EXPORT void chiaki_ctrl_fini(ChiakiCtrl *ctrl); CHIAKI_EXPORT ChiakiErrorCode chiaki_ctrl_send_message(ChiakiCtrl *ctrl, uint16_t type, const uint8_t *payload, size_t payload_size); CHIAKI_EXPORT void chiaki_ctrl_set_login_pin(ChiakiCtrl *ctrl, const uint8_t *pin, size_t pin_size); CHIAKI_EXPORT ChiakiErrorCode chiaki_ctrl_goto_bed(ChiakiCtrl *ctrl); +CHIAKI_EXPORT ChiakiErrorCode chiaki_ctrl_keyboard_set_text(ChiakiCtrl *ctrl, const char* text); +CHIAKI_EXPORT ChiakiErrorCode chiaki_ctrl_keyboard_accept(ChiakiCtrl *ctrl); +CHIAKI_EXPORT ChiakiErrorCode chiaki_ctrl_keyboard_reject(ChiakiCtrl *ctrl); #ifdef __cplusplus } diff --git a/lib/include/chiaki/session.h b/lib/include/chiaki/session.h index d3c7b1f..558d045 100644 --- a/lib/include/chiaki/session.h +++ b/lib/include/chiaki/session.h @@ -102,6 +102,11 @@ typedef struct chiaki_quit_event_t const char *reason_str; } ChiakiQuitEvent; +typedef struct chiaki_keyboard_event_t +{ + const char *text_str; +} ChiakiKeyboardEvent; + typedef struct chiaki_audio_stream_info_event_t { ChiakiAudioHeader audio_header; @@ -111,7 +116,10 @@ typedef struct chiaki_audio_stream_info_event_t typedef enum { CHIAKI_EVENT_CONNECTED, CHIAKI_EVENT_LOGIN_PIN_REQUEST, - CHIAKI_EVENT_QUIT + CHIAKI_EVENT_KEYBOARD_OPEN, + CHIAKI_EVENT_KEYBOARD_TEXT_CHANGE, + CHIAKI_EVENT_KEYBOARD_REMOTE_CLOSE, + CHIAKI_EVENT_QUIT, } ChiakiEventType; typedef struct chiaki_event_t @@ -120,6 +128,7 @@ typedef struct chiaki_event_t union { ChiakiQuitEvent quit; + ChiakiKeyboardEvent keyboard; struct { bool pin_incorrect; // false on first request, true if the pin entered before was incorrect @@ -202,6 +211,9 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_session_join(ChiakiSession *session); CHIAKI_EXPORT ChiakiErrorCode chiaki_session_set_controller_state(ChiakiSession *session, ChiakiControllerState *state); CHIAKI_EXPORT ChiakiErrorCode chiaki_session_set_login_pin(ChiakiSession *session, const uint8_t *pin, size_t pin_size); CHIAKI_EXPORT ChiakiErrorCode chiaki_session_goto_bed(ChiakiSession *session); +CHIAKI_EXPORT ChiakiErrorCode chiaki_session_keyboard_set_text(ChiakiSession *session, const char *text); +CHIAKI_EXPORT ChiakiErrorCode chiaki_session_keyboard_reject(ChiakiSession *session); +CHIAKI_EXPORT ChiakiErrorCode chiaki_session_keyboard_accept(ChiakiSession *session); static inline void chiaki_session_set_event_cb(ChiakiSession *session, ChiakiEventCallback cb, void *user) { diff --git a/lib/src/ctrl.c b/lib/src/ctrl.c index b410741..0f5e4a1 100644 --- a/lib/src/ctrl.c +++ b/lib/src/ctrl.c @@ -36,7 +36,13 @@ typedef enum ctrl_message_type_t { CTRL_MESSAGE_TYPE_LOGIN_PIN_REQ = 0x4, CTRL_MESSAGE_TYPE_LOGIN_PIN_REP = 0x8004, CTRL_MESSAGE_TYPE_LOGIN = 0x5, - CTRL_MESSAGE_TYPE_GOTO_BED = 0x50 + CTRL_MESSAGE_TYPE_GOTO_BED = 0x50, + CTRL_MESSAGE_TYPE_KEYBOARD_ENABLE_TOGGLE = 0x20, + CTRL_MESSAGE_TYPE_KEYBOARD_OPEN = 0x21, + CTRL_MESSAGE_TYPE_KEYBOARD_CLOSE_REMOTE = 0x22, + CTRL_MESSAGE_TYPE_KEYBOARD_TEXT_CHANGE_REQ = 0x23, + CTRL_MESSAGE_TYPE_KEYBOARD_TEXT_CHANGE_RES = 0x24, + CTRL_MESSAGE_TYPE_KEYBOARD_CLOSE_REQ = 0x25, } CtrlMessageType; typedef enum ctrl_login_state_t { @@ -52,12 +58,44 @@ struct chiaki_ctrl_message_queue_t size_t payload_size; }; +typedef struct ctrl_keyboard_open_t +{ + uint8_t unk[0x1C]; + uint32_t text_length; +} CtrlKeyboardOpenMessage; + +typedef struct ctrl_keyboard_text_request_t +{ + uint32_t counter; + uint32_t text_length1; + uint8_t unk1[0x8]; + uint8_t unk2[0x10]; + uint32_t text_length2; +} CtrlKeyboardTextRequestMessage; + +typedef struct ctrl_keyboard_text_response_t +{ + uint32_t counter; + uint32_t unk; + uint32_t text_length1; + uint32_t unk2; + uint8_t unk3[0x10]; + uint32_t unk4; + uint32_t text_length2; +} CtrlKeyboardTextResponseMessage; + +void chiaki_session_send_event(ChiakiSession *session, ChiakiEvent *event); + static void *ctrl_thread_func(void *user); static ChiakiErrorCode ctrl_message_send(ChiakiCtrl *ctrl, uint16_t type, const uint8_t *payload, size_t payload_size); +static void ctrl_enable_optional_features(ChiakiCtrl *ctrl); static void ctrl_message_received_session_id(ChiakiCtrl *ctrl, uint8_t *payload, size_t payload_size); static void ctrl_message_received_heartbeat_req(ChiakiCtrl *ctrl, uint8_t *payload, size_t payload_size); static void ctrl_message_received_login_pin_req(ChiakiCtrl *ctrl, uint8_t *payload, size_t payload_size); static void ctrl_message_received_login(ChiakiCtrl *ctrl, uint8_t *payload, size_t payload_size); +static void ctrl_message_received_keyboard_open(ChiakiCtrl *ctrl, uint8_t *payload, size_t payload_size); +static void ctrl_message_received_keyboard_close(ChiakiCtrl *ctrl, uint8_t *payload, size_t payload_size); +static void ctrl_message_received_keyboard_text_change(ChiakiCtrl *ctrl, uint8_t *payload, size_t payload_size); CHIAKI_EXPORT ChiakiErrorCode chiaki_ctrl_init(ChiakiCtrl *ctrl, ChiakiSession *session) { @@ -69,6 +107,7 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_ctrl_init(ChiakiCtrl *ctrl, ChiakiSession * ctrl->login_pin = NULL; ctrl->login_pin_size = 0; ctrl->msg_queue = NULL; + ctrl->keyboard_text_counter = 0; ChiakiErrorCode err = chiaki_stop_pipe_init(&ctrl->notif_pipe); if(err != CHIAKI_ERR_SUCCESS) @@ -137,6 +176,7 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_ctrl_send_message(ChiakiCtrl *ctrl, uint16_ free(queue); return CHIAKI_ERR_MEMORY; } + memcpy(queue->payload, payload, payload_size); queue->payload_size = payload_size; } else @@ -182,6 +222,41 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_ctrl_goto_bed(ChiakiCtrl *ctrl) return chiaki_ctrl_send_message(ctrl, CTRL_MESSAGE_TYPE_GOTO_BED, NULL, 0); } +CHIAKI_EXPORT ChiakiErrorCode chiaki_ctrl_keyboard_set_text(ChiakiCtrl *ctrl, const char *text) +{ + const uint32_t length = strlen(text); + const size_t payload_size = sizeof(CtrlKeyboardTextRequestMessage) + length; + + uint8_t *payload = malloc(payload_size); + if(!payload) + return CHIAKI_ERR_MEMORY; + memset(payload, 0, payload_size); + memcpy(payload + sizeof(CtrlKeyboardTextRequestMessage), text, length); + + CtrlKeyboardTextRequestMessage *msg = (CtrlKeyboardTextRequestMessage *)payload; + msg->counter = ntohl(++ctrl->keyboard_text_counter); + msg->text_length1 = ntohl(length); + msg->text_length2 = ntohl(length); + + ChiakiErrorCode err; + err = chiaki_ctrl_send_message(ctrl, CTRL_MESSAGE_TYPE_KEYBOARD_TEXT_CHANGE_REQ, payload, payload_size); + + free(payload); + return err; +} + +CHIAKI_EXPORT ChiakiErrorCode chiaki_ctrl_keyboard_accept(ChiakiCtrl *ctrl) +{ + const uint8_t accept[4] = { 0x00, 0x00, 0x00, 0x00 }; + return chiaki_ctrl_send_message(ctrl, CTRL_MESSAGE_TYPE_KEYBOARD_CLOSE_REQ, accept, 4); +} + +CHIAKI_EXPORT ChiakiErrorCode chiaki_ctrl_keyboard_reject(ChiakiCtrl *ctrl) +{ + const uint8_t reject[4] = { 0x00, 0x00, 0x00, 0x01 }; + return chiaki_ctrl_send_message(ctrl, CTRL_MESSAGE_TYPE_KEYBOARD_CLOSE_REQ, reject, 4); +} + static ChiakiErrorCode ctrl_connect(ChiakiCtrl *ctrl); static void ctrl_message_received(ChiakiCtrl *ctrl, uint16_t msg_type, uint8_t *payload, size_t payload_size); @@ -248,6 +323,8 @@ static void *ctrl_thread_func(void *user) chiaki_mutex_unlock(&ctrl->notif_mutex); err = chiaki_stop_pipe_select_single(&ctrl->notif_pipe, ctrl->sock, false, UINT64_MAX); chiaki_mutex_lock(&ctrl->notif_mutex); + + bool msg_queue_updated = false; if(err == CHIAKI_ERR_CANCELED) { while(ctrl->msg_queue) @@ -256,6 +333,7 @@ static void *ctrl_thread_func(void *user) ChiakiCtrlMessageQueue *next = ctrl->msg_queue->next; ctrl_message_queue_free(ctrl->msg_queue); ctrl->msg_queue = next; + msg_queue_updated = true; } if(ctrl->login_pin_entered) @@ -276,6 +354,9 @@ static void *ctrl_thread_func(void *user) break; } + if(msg_queue_updated) + chiaki_stop_pipe_reset(&ctrl->notif_pipe); + continue; } else if(err != CHIAKI_ERR_SUCCESS) @@ -378,6 +459,7 @@ static void ctrl_message_received(ChiakiCtrl *ctrl, uint16_t msg_type, uint8_t * { case CTRL_MESSAGE_TYPE_SESSION_ID: ctrl_message_received_session_id(ctrl, payload, payload_size); + ctrl_enable_optional_features(ctrl); break; case CTRL_MESSAGE_TYPE_HEARTBEAT_REQ: ctrl_message_received_heartbeat_req(ctrl, payload, payload_size); @@ -388,13 +470,36 @@ static void ctrl_message_received(ChiakiCtrl *ctrl, uint16_t msg_type, uint8_t * case CTRL_MESSAGE_TYPE_LOGIN: ctrl_message_received_login(ctrl, payload, payload_size); break; - default: + case CTRL_MESSAGE_TYPE_KEYBOARD_OPEN: + ctrl_message_received_keyboard_open(ctrl, payload, payload_size); + break; + case CTRL_MESSAGE_TYPE_KEYBOARD_TEXT_CHANGE_RES: + ctrl_message_received_keyboard_text_change(ctrl, payload, payload_size); + break; + case CTRL_MESSAGE_TYPE_KEYBOARD_CLOSE_REMOTE: + ctrl_message_received_keyboard_close(ctrl, payload, payload_size); + break; + default: CHIAKI_LOGW(ctrl->session->log, "Received Ctrl Message with unknown type %#x", msg_type); chiaki_log_hexdump(ctrl->session->log, CHIAKI_LOG_WARNING, payload, payload_size); break; } } +static void ctrl_enable_optional_features(ChiakiCtrl *ctrl) +{ + // TODO: Make this optional. + // TODO: Last byte of pre_enable request is random (?) + // TODO: Signature ?! + uint8_t enable = 1; + uint8_t pre_enable[4] = { 0x00, 0x01, 0x01, 0x80 }; + uint8_t signature[0x10] = { 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x05, 0xAE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; + ctrl_message_send(ctrl, 0xD, signature, 0x10); + ctrl_message_send(ctrl, 0x36, pre_enable, 4); + ctrl_message_send(ctrl, CTRL_MESSAGE_TYPE_KEYBOARD_ENABLE_TOGGLE, &enable, 1); + ctrl_message_send(ctrl, 0x36, pre_enable, 4); +} + static void ctrl_message_received_session_id(ChiakiCtrl *ctrl, uint8_t *payload, size_t payload_size) { @@ -511,6 +616,64 @@ static void ctrl_message_received_login(ChiakiCtrl *ctrl, uint8_t *payload, size } } +static void ctrl_message_received_keyboard_open(ChiakiCtrl *ctrl, uint8_t *payload, size_t payload_size) +{ + assert(payload_size >= sizeof(CtrlKeyboardOpenMessage)); + + CtrlKeyboardOpenMessage *msg = (CtrlKeyboardOpenMessage *)payload; + msg->text_length = ntohl(msg->text_length); + assert(payload_size == sizeof(CtrlKeyboardOpenMessage) + msg->text_length); + + uint8_t *buffer = msg->text_length > 0 ? malloc((size_t)msg->text_length + 1) : NULL; + if(buffer) + { + buffer[msg->text_length] = '\0'; + memcpy(buffer, payload + sizeof(CtrlKeyboardOpenMessage), msg->text_length); + } + + ChiakiEvent keyboard_event; + keyboard_event.type = CHIAKI_EVENT_KEYBOARD_OPEN; + keyboard_event.keyboard.text_str = (const char *)buffer; + chiaki_session_send_event(ctrl->session, &keyboard_event); + + if(buffer) + free(buffer); +} + +static void ctrl_message_received_keyboard_close(ChiakiCtrl *ctrl, uint8_t *payload, size_t payload_size) +{ + (void)payload; + (void)payload_size; + + ChiakiEvent keyboard_event; + keyboard_event.type = CHIAKI_EVENT_KEYBOARD_REMOTE_CLOSE; + keyboard_event.keyboard.text_str = NULL; + chiaki_session_send_event(ctrl->session, &keyboard_event); +} + +static void ctrl_message_received_keyboard_text_change(ChiakiCtrl* ctrl, uint8_t* payload, size_t payload_size) +{ + assert(payload_size >= sizeof(CtrlKeyboardTextResponseMessage)); + + CtrlKeyboardTextResponseMessage *msg = (CtrlKeyboardTextResponseMessage *)payload; + msg->text_length1 = ntohl(msg->text_length1); + assert(payload_size == sizeof(CtrlKeyboardTextResponseMessage) + msg->text_length1); + + uint8_t *buffer = msg->text_length1 > 0 ? malloc((size_t)msg->text_length1 + 1) : NULL; + if(buffer) + { + buffer[msg->text_length1] = '\0'; + memcpy(buffer, payload + sizeof(CtrlKeyboardTextResponseMessage), msg->text_length1); + } + + ChiakiEvent keyboard_event; + keyboard_event.type = CHIAKI_EVENT_KEYBOARD_TEXT_CHANGE; + keyboard_event.keyboard.text_str = (const char *)buffer; + chiaki_session_send_event(ctrl->session, &keyboard_event); + + if(buffer) + free(buffer); +} typedef struct ctrl_response_t { bool server_type_valid; diff --git a/lib/src/session.c b/lib/src/session.c index fd1411d..58f528c 100644 --- a/lib/src/session.c +++ b/lib/src/session.c @@ -822,3 +822,18 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_session_goto_bed(ChiakiSession *session) { return chiaki_ctrl_goto_bed(&session->ctrl); } + +CHIAKI_EXPORT ChiakiErrorCode chiaki_session_keyboard_set_text(ChiakiSession *session, const char *text) +{ + return chiaki_ctrl_keyboard_set_text(&session->ctrl, text); +} + +CHIAKI_EXPORT ChiakiErrorCode chiaki_session_keyboard_reject(ChiakiSession *session) +{ + return chiaki_ctrl_keyboard_reject(&session->ctrl); +} + +CHIAKI_EXPORT ChiakiErrorCode chiaki_session_keyboard_accept(ChiakiSession *session) +{ + return chiaki_ctrl_keyboard_accept(&session->ctrl); +} From aea23effe95ed5709d1c6985eb8c36d447b8a68a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Sat, 7 Nov 2020 11:17:22 +0100 Subject: [PATCH 066/237] Fix Bullseye Deps (#363) --- scripts/Dockerfile.bullseye | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/Dockerfile.bullseye b/scripts/Dockerfile.bullseye index 4946fd0..96cab09 100644 --- a/scripts/Dockerfile.bullseye +++ b/scripts/Dockerfile.bullseye @@ -6,7 +6,7 @@ RUN apt-get -y install git g++ cmake pkg-config \ libjerasure-dev nanopb libnanopb-dev libavcodec-dev libopus-dev \ libssl-dev protobuf-compiler python3 python3-protobuf \ libevdev-dev libudev-dev \ - qt5-default libqt5svg5-dev qtmultimedia5-dev libsdl2-dev + libqt5opengl5-dev libqt5svg5-dev qtmultimedia5-dev libsdl2-dev CMD [] From a2ebf7e40816778c982d49b2566e413db83f8fdb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Wed, 11 Nov 2020 14:03:56 +0100 Subject: [PATCH 067/237] Disable Keyboard by default --- android/app/src/main/cpp/chiaki-jni.c | 2 +- gui/include/streamsession.h | 1 + gui/src/streamsession.cpp | 1 + lib/include/chiaki/session.h | 2 ++ lib/src/ctrl.c | 3 ++- lib/src/session.c | 1 + switch/src/host.cpp | 2 +- 7 files changed, 9 insertions(+), 3 deletions(-) diff --git a/android/app/src/main/cpp/chiaki-jni.c b/android/app/src/main/cpp/chiaki-jni.c index 195dfe5..b970ddc 100644 --- a/android/app/src/main/cpp/chiaki-jni.c +++ b/android/app/src/main/cpp/chiaki-jni.c @@ -204,7 +204,7 @@ JNIEXPORT void JNICALL JNI_FCN(sessionCreate)(JNIEnv *env, jobject obj, jobject jobject connect_video_profile_obj = E->GetObjectField(env, connect_info_obj, E->GetFieldID(env, connect_info_class, "videoProfile", "L"BASE_PACKAGE"/ConnectVideoProfile;")); jclass connect_video_profile_class = E->GetObjectClass(env, connect_video_profile_obj); - ChiakiConnectInfo connect_info; + ChiakiConnectInfo connect_info = { 0 }; const char *str_borrow = E->GetStringUTFChars(env, host_string, NULL); connect_info.host = host_str = strdup(str_borrow); E->ReleaseStringUTFChars(env, host_string, str_borrow); diff --git a/gui/include/streamsession.h b/gui/include/streamsession.h index 295998c..82acf86 100644 --- a/gui/include/streamsession.h +++ b/gui/include/streamsession.h @@ -44,6 +44,7 @@ struct StreamSessionConnectInfo ChiakiConnectVideoProfile video_profile; unsigned int audio_buffer_size; bool fullscreen; + bool enable_keyboard; StreamSessionConnectInfo(Settings *settings, QString host, QByteArray regist_key, QByteArray morning, bool fullscreen); }; diff --git a/gui/src/streamsession.cpp b/gui/src/streamsession.cpp index a6745ef..f26f9a2 100644 --- a/gui/src/streamsession.cpp +++ b/gui/src/streamsession.cpp @@ -27,6 +27,7 @@ StreamSessionConnectInfo::StreamSessionConnectInfo(Settings *settings, QString h this->morning = morning; audio_buffer_size = settings->GetAudioBufferSize(); this->fullscreen = fullscreen; + this->enable_keyboard = false; // TODO: from settings } static void AudioSettingsCb(uint32_t channels, uint32_t rate, void *user); diff --git a/lib/include/chiaki/session.h b/lib/include/chiaki/session.h index 558d045..80c89d3 100644 --- a/lib/include/chiaki/session.h +++ b/lib/include/chiaki/session.h @@ -76,6 +76,7 @@ typedef struct chiaki_connect_info_t char regist_key[CHIAKI_SESSION_AUTH_SIZE]; // must be completely filled (pad with \0) uint8_t morning[0x10]; ChiakiConnectVideoProfile video_profile; + bool enable_keyboard; } ChiakiConnectInfo; @@ -157,6 +158,7 @@ typedef struct chiaki_session_t uint8_t morning[CHIAKI_RPCRYPT_KEY_SIZE]; uint8_t did[CHIAKI_RP_DID_SIZE]; ChiakiConnectVideoProfile video_profile; + bool enable_keyboard; } connect_info; ChiakiTarget target; diff --git a/lib/src/ctrl.c b/lib/src/ctrl.c index 0f5e4a1..1c83568 100644 --- a/lib/src/ctrl.c +++ b/lib/src/ctrl.c @@ -488,7 +488,8 @@ static void ctrl_message_received(ChiakiCtrl *ctrl, uint16_t msg_type, uint8_t * static void ctrl_enable_optional_features(ChiakiCtrl *ctrl) { - // TODO: Make this optional. + if(!ctrl->session->connect_info.enable_keyboard) + return; // TODO: Last byte of pre_enable request is random (?) // TODO: Signature ?! uint8_t enable = 1; diff --git a/lib/src/session.c b/lib/src/session.c index 58f528c..496893d 100644 --- a/lib/src/session.c +++ b/lib/src/session.c @@ -215,6 +215,7 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_session_init(ChiakiSession *session, Chiaki memcpy(session->connect_info.did + sizeof(session->connect_info.did) - sizeof(did_suffix), did_suffix, sizeof(did_suffix)); session->connect_info.video_profile = connect_info->video_profile; + session->connect_info.enable_keyboard = connect_info->enable_keyboard; return CHIAKI_ERR_SUCCESS; error_stop_pipe: diff --git a/switch/src/host.cpp b/switch/src/host.cpp index e0d161a..200f74d 100644 --- a/switch/src/host.cpp +++ b/switch/src/host.cpp @@ -136,7 +136,7 @@ int Host::InitSession(IO * user) // Build chiaki ps4 stream session chiaki_opus_decoder_init(&(this->opus_decoder), this->log); ChiakiAudioSink audio_sink; - ChiakiConnectInfo chiaki_connect_info; + ChiakiConnectInfo chiaki_connect_info = { 0 }; chiaki_connect_info.host = this->host_addr.c_str(); chiaki_connect_info.video_profile = this->video_profile; From 8b6f9a5fc1ce5150999f123ce8db7fc230821a51 Mon Sep 17 00:00:00 2001 From: Roy P Date: Thu, 12 Nov 2020 10:41:26 -0500 Subject: [PATCH 068/237] Updating flatpak to point to 1.3.0 branch (#368) --- scripts/flatpak/com.github.thestr4ng3r.Chiaki.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/flatpak/com.github.thestr4ng3r.Chiaki.json b/scripts/flatpak/com.github.thestr4ng3r.Chiaki.json index abd3710..905ac2a 100644 --- a/scripts/flatpak/com.github.thestr4ng3r.Chiaki.json +++ b/scripts/flatpak/com.github.thestr4ng3r.Chiaki.json @@ -89,8 +89,8 @@ { "type": "git", "url": "https://github.com/thestr4ng3r/chiaki.git", - "tag": "v1.2.1", - "commit": "89c9bd44c354d1351fb9b9a8f9f0aade8077a129" + "tag": "v1.3.0", + "commit": "702d31eb01d37518e77f5c1be3ea493df9f18323" } ] } From ea79836f0f40a8cc401768933137964c603dd3c5 Mon Sep 17 00:00:00 2001 From: Blueroom VR Date: Sat, 14 Nov 2020 10:51:08 -0800 Subject: [PATCH 069/237] Add Raspberry Pi Decoder (Fix #126) (#360) --- CMakeLists.txt | 22 +++ cmake/FindILClient.cmake | 57 +++++++ gui/include/settings.h | 9 ++ gui/include/settingsdialog.h | 2 + gui/include/streamsession.h | 16 +- gui/include/streamwindow.h | 4 + gui/src/settings.cpp | 24 +++ gui/src/settingsdialog.cpp | 20 +++ gui/src/streamsession.cpp | 44 +++++- gui/src/streamwindow.cpp | 44 +++++- lib/CMakeLists.txt | 10 ++ lib/config.h.in | 1 + lib/include/chiaki/pidecoder.h | 36 +++++ lib/src/pidecoder.c | 261 +++++++++++++++++++++++++++++++++ 14 files changed, 543 insertions(+), 7 deletions(-) create mode 100644 cmake/FindILClient.cmake create mode 100644 lib/include/chiaki/pidecoder.h create mode 100644 lib/src/pidecoder.c diff --git a/CMakeLists.txt b/CMakeLists.txt index 57d4c0e..1814800 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -16,6 +16,7 @@ option(CHIAKI_ENABLE_ANDROID "Enable Android (Use only as part of the Gradle Pro option(CHIAKI_ENABLE_SWITCH "Enable Nintendo Switch (Requires devKitPro libnx)" OFF) tri_option(CHIAKI_ENABLE_SETSU "Enable libsetsu for touchpad input from controller" AUTO) option(CHIAKI_LIB_ENABLE_OPUS "Use Opus as part of Chiaki Lib" ON) +tri_option(CHIAKI_ENABLE_PI_DECODER "Enable Raspberry Pi-specific video decoder (requires libraspberrypi0 and libraspberrypi-doc)" AUTO) option(CHIAKI_LIB_ENABLE_MBEDTLS "Use mbedtls instead of OpenSSL as part of Chiaki Lib" OFF) option(CHIAKI_LIB_OPENSSL_EXTERNAL_PROJECT "Use OpenSSL as CMake external project" OFF) option(CHIAKI_GUI_ENABLE_SDL_GAMECONTROLLER "Use SDL Gamecontroller for Input" ON) @@ -99,6 +100,27 @@ if(CHIAKI_LIB_ENABLE_MBEDTLS) add_definitions(-DCHIAKI_LIB_ENABLE_MBEDTLS) endif() +if(CHIAKI_ENABLE_PI_DECODER) + find_package(ILClient) + if(ILClient_FOUND) + set(CHIAKI_ENABLE_PI_DECODER ON) + else() + if(NOT CHIAKI_ENABLE_PI_DECODER STREQUAL AUTO) + message(FATAL_ERROR " +CHIAKI_ENABLE_PI_DECODER is set to ON, but its dependencies (ilclient source and libs) could not be resolved. +The Raspberry Pi Decoder is only supported on Raspberry Pi OS and requires libraspberrypi0 and libraspberrypi-doc.") + endif() + set(CHIAKI_ENABLE_PI_DECODER OFF) + endif() +endif() + +if(CHIAKI_ENABLE_PI_DECODER) + message(STATUS "Pi Decoder enabled") +else() + message(STATUS "Pi Decoder disabled") +endif() + + add_subdirectory(lib) if(CHIAKI_ENABLE_CLI) diff --git a/cmake/FindILClient.cmake b/cmake/FindILClient.cmake new file mode 100644 index 0000000..b24fccd --- /dev/null +++ b/cmake/FindILClient.cmake @@ -0,0 +1,57 @@ +# Provides ILClient::ILClient +# (Raspberry Pi-specific video decoding stuff, very specific for libraspberrypi0 and libraspberrypi-doc) + +set(_required_libs + /opt/vc/lib/libbcm_host.so + /opt/vc/lib/libvcilcs.a + /opt/vc/lib/libvchiq_arm.so + /opt/vc/lib/libvcos.so) + +unset(_libvars) +foreach(_lib ${_required_libs}) + get_filename_component(_libname "${_lib}" NAME_WE) + set(_libvar "ILClient_${_libname}_LIBRARY") + list(APPEND _libvars "${_libvar}") + if(EXISTS "${_lib}") + set("${_libvar}" "${_lib}") + else() + set("${_libvar}" "${_libvar}-NOTFOUND") + endif() +endforeach() + +find_path(ILClient_INCLUDE_DIR bcm_host.h + PATHS /opt/vc/include + NO_DEFAULT_PATH) + +find_path(ILClient_SOURCE_DIR ilclient.c + PATHS /opt/vc/src/hello_pi/libs/ilclient) + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(ILClient + FOUND_VAR ILClient_FOUND + REQUIRED_VARS + ${_libvars} + ILClient_INCLUDE_DIR + ILClient_SOURCE_DIR) + +if(ILClient_FOUND) + if(NOT TARGET ILClient::ILClient) + # see /opt/vc/src/hello_pi/libs/ilclient/Makefile + add_library(ilclient STATIC + "${ILClient_SOURCE_DIR}/ilclient.c" + "${ILClient_SOURCE_DIR}/ilcore.c") + target_include_directories(ilclient PUBLIC + "${ILClient_INCLUDE_DIR}" + "${ILClient_SOURCE_DIR}") + target_compile_definitions(ilclient PUBLIC + HAVE_LIBOPENMAX=2 + OMX + OMX_SKIP64BIT + USE_EXTERNAL_OMX + HAVE_LIBBCM_HOST + USE_EXTERNAL_LIBBCM_HOST + USE_VCHIQ_ARM) + target_link_libraries(ilclient PUBLIC ${_required_libs}) + add_library(ILClient::ILClient ALIAS ilclient) + endif() +endif() diff --git a/gui/include/settings.h b/gui/include/settings.h index d2cee79..0d5975a 100644 --- a/gui/include/settings.h +++ b/gui/include/settings.h @@ -30,6 +30,12 @@ enum class DisconnectAction Ask }; +enum class Decoder +{ + Ffmpeg, + Pi +}; + class Settings : public QObject { Q_OBJECT @@ -69,6 +75,9 @@ class Settings : public QObject unsigned int GetBitrate() const; void SetBitrate(unsigned int bitrate); + Decoder GetDecoder() const; + void SetDecoder(Decoder decoder); + HardwareDecodeEngine GetHardwareDecodeEngine() const; void SetHardwareDecodeEngine(HardwareDecodeEngine enabled); diff --git a/gui/include/settingsdialog.h b/gui/include/settingsdialog.h index a44e209..b0b529e 100644 --- a/gui/include/settingsdialog.h +++ b/gui/include/settingsdialog.h @@ -25,6 +25,7 @@ class SettingsDialog : public QDialog QComboBox *fps_combo_box; QLineEdit *bitrate_edit; QLineEdit *audio_buffer_size_edit; + QCheckBox *pi_decoder_check_box; QComboBox *hardware_decode_combo_box; QListWidget *registered_hosts_list_widget; @@ -41,6 +42,7 @@ class SettingsDialog : public QDialog void BitrateEdited(); void AudioBufferSizeEdited(); void HardwareDecodeEngineSelected(); + void UpdateHardwareDecodeEngineComboBox(); void UpdateRegisteredHosts(); void UpdateRegisteredHostsButtons(); diff --git a/gui/include/streamsession.h b/gui/include/streamsession.h index 82acf86..2d5cb32 100644 --- a/gui/include/streamsession.h +++ b/gui/include/streamsession.h @@ -6,6 +6,10 @@ #include #include +#if CHIAKI_LIB_ENABLE_PI_DECODER +#include +#endif + #if CHIAKI_GUI_ENABLE_SETSU #include #endif @@ -14,6 +18,7 @@ #include "exception.h" #include "sessionlog.h" #include "controllermanager.h" +#include "settings.h" #include #include @@ -35,6 +40,7 @@ struct StreamSessionConnectInfo { Settings *settings; QMap key_map; + Decoder decoder; HardwareDecodeEngine hw_decode_engine; uint32_t log_level_mask; QString log_file; @@ -70,7 +76,10 @@ class StreamSession : public QObject ChiakiControllerState keyboard_state; - VideoDecoder video_decoder; + VideoDecoder *video_decoder; +#if CHIAKI_LIB_ENABLE_PI_DECODER + ChiakiPiDecoder *pi_decoder; +#endif unsigned int audio_buffer_size; QAudioOutput *audio_output; @@ -101,7 +110,10 @@ class StreamSession : public QObject void SetLoginPIN(const QString &pin); Controller *GetController() { return controller; } - VideoDecoder *GetVideoDecoder() { return &video_decoder; } + VideoDecoder *GetVideoDecoder() { return video_decoder; } +#if CHIAKI_LIB_ENABLE_PI_DECODER + ChiakiPiDecoder *GetPiDecoder() { return pi_decoder; } +#endif void HandleKeyboardEvent(QKeyEvent *event); void HandleMouseEvent(QMouseEvent *event); diff --git a/gui/include/streamwindow.h b/gui/include/streamwindow.h index 63c7440..5476672 100644 --- a/gui/include/streamwindow.h +++ b/gui/include/streamwindow.h @@ -25,6 +25,7 @@ class StreamWindow: public QMainWindow AVOpenGLWidget *av_widget; void Init(); + void UpdateVideoTransform(); protected: void keyPressEvent(QKeyEvent *event) override; @@ -32,6 +33,9 @@ class StreamWindow: public QMainWindow void closeEvent(QCloseEvent *event) override; void mousePressEvent(QMouseEvent *event) override; void mouseReleaseEvent(QMouseEvent *event) override; + void resizeEvent(QResizeEvent *event) override; + void moveEvent(QMoveEvent *event) override; + void changeEvent(QEvent *event) override; private slots: void SessionQuit(ChiakiQuitReason reason, const QString &reason_str); diff --git a/gui/src/settings.cpp b/gui/src/settings.cpp index 40bf298..bedce8d 100644 --- a/gui/src/settings.cpp +++ b/gui/src/settings.cpp @@ -3,6 +3,8 @@ #include #include +#include + #define SETTINGS_VERSION 1 Settings::Settings(QObject *parent) : QObject(parent) @@ -79,6 +81,28 @@ unsigned int Settings::GetAudioBufferSizeRaw() const return settings.value("settings/audio_buffer_size", 0).toUInt(); } +static const QMap decoder_values = { + { Decoder::Ffmpeg, "ffmpeg" }, + { Decoder::Pi, "pi" } +}; + +static const Decoder decoder_default = Decoder::Pi; + +Decoder Settings::GetDecoder() const +{ +#if CHIAKI_LIB_ENABLE_PI_DECODER + auto v = settings.value("settings/decoder", decoder_values[decoder_default]).toString(); + return decoder_values.key(v, decoder_default); +#else + return Decoder::Ffmpeg; +#endif +} + +void Settings::SetDecoder(Decoder decoder) +{ + settings.setValue("settings/decoder", decoder_values[decoder]); +} + static const QMap hw_decode_engine_values = { { HW_DECODE_NONE, "none" }, { HW_DECODE_VAAPI, "vaapi" }, diff --git a/gui/src/settingsdialog.cpp b/gui/src/settingsdialog.cpp index f16d110..2e96b83 100644 --- a/gui/src/settingsdialog.cpp +++ b/gui/src/settingsdialog.cpp @@ -20,6 +20,8 @@ #include #include +#include + const char * const about_string = "

Chiaki

by thestr4ng3r, version " CHIAKI_VERSION "" @@ -157,6 +159,18 @@ SettingsDialog::SettingsDialog(Settings *settings, QWidget *parent) : QDialog(pa auto decode_settings_layout = new QFormLayout(); decode_settings->setLayout(decode_settings_layout); +#if CHIAKI_LIB_ENABLE_PI_DECODER + pi_decoder_check_box = new QCheckBox(this); + pi_decoder_check_box->setChecked(settings->GetDecoder() == Decoder::Pi); + connect(pi_decoder_check_box, &QCheckBox::toggled, this, [this](bool checked) { + this->settings->SetDecoder(checked ? Decoder::Pi : Decoder::Ffmpeg); + UpdateHardwareDecodeEngineComboBox(); + }); + decode_settings_layout->addRow(tr("Use Raspberry Pi Decoder:"), pi_decoder_check_box); +#else + pi_decoder_check_box = nullptr; +#endif + hardware_decode_combo_box = new QComboBox(this); static const QList> hardware_decode_engines = { { HW_DECODE_NONE, "none"}, @@ -173,6 +187,7 @@ SettingsDialog::SettingsDialog(Settings *settings, QWidget *parent) : QDialog(pa } connect(hardware_decode_combo_box, SIGNAL(currentIndexChanged(int)), this, SLOT(HardwareDecodeEngineSelected())); decode_settings_layout->addRow(tr("Hardware decode method:"), hardware_decode_combo_box); + UpdateHardwareDecodeEngineComboBox(); // Registered Consoles @@ -281,6 +296,11 @@ void SettingsDialog::HardwareDecodeEngineSelected() settings->SetHardwareDecodeEngine((HardwareDecodeEngine)hardware_decode_combo_box->currentData().toInt()); } +void SettingsDialog::UpdateHardwareDecodeEngineComboBox() +{ + hardware_decode_combo_box->setEnabled(settings->GetDecoder() == Decoder::Ffmpeg); +} + void SettingsDialog::UpdateBitratePlaceholder() { bitrate_edit->setPlaceholderText(tr("Automatic (%1)").arg(settings->GetVideoProfile().bitrate)); diff --git a/gui/src/streamsession.cpp b/gui/src/streamsession.cpp index f26f9a2..bb67f91 100644 --- a/gui/src/streamsession.cpp +++ b/gui/src/streamsession.cpp @@ -18,6 +18,7 @@ StreamSessionConnectInfo::StreamSessionConnectInfo(Settings *settings, QString h : settings(settings) { key_map = settings->GetControllerMappingForDecoding(); + decoder = settings->GetDecoder(); hw_decode_engine = settings->GetHardwareDecodeEngine(); log_level_mask = settings->GetLogLevelMask(); log_file = CreateLogFilename(); @@ -42,11 +43,30 @@ StreamSession::StreamSession(const StreamSessionConnectInfo &connect_info, QObje : QObject(parent), log(this, connect_info.log_level_mask, connect_info.log_file), controller(nullptr), - video_decoder(connect_info.hw_decode_engine, log.GetChiakiLog()), + video_decoder(nullptr), +#if CHIAKI_LIB_ENABLE_PI_DECODER + pi_decoder(nullptr), +#endif audio_output(nullptr), audio_io(nullptr) { connected = false; + +#if CHIAKI_LIB_ENABLE_PI_DECODER + if(connect_info.decoder == Decoder::Pi) + { + pi_decoder = CHIAKI_NEW(ChiakiPiDecoder); + if(chiaki_pi_decoder_init(pi_decoder, log.GetChiakiLog()) != CHIAKI_ERR_SUCCESS) + throw ChiakiException("Failed to initialize Raspberry Pi Decoder"); + } + else + { +#endif + video_decoder = new VideoDecoder(connect_info.hw_decode_engine, log.GetChiakiLog()); +#if CHIAKI_LIB_ENABLE_PI_DECODER + } +#endif + chiaki_opus_decoder_init(&opus_decoder, log.GetChiakiLog()); audio_buffer_size = connect_info.audio_buffer_size; @@ -75,7 +95,17 @@ StreamSession::StreamSession(const StreamSessionConnectInfo &connect_info, QObje chiaki_opus_decoder_get_sink(&opus_decoder, &audio_sink); chiaki_session_set_audio_sink(&session, &audio_sink); - chiaki_session_set_video_sample_cb(&session, VideoSampleCb, this); +#if CHIAKI_LIB_ENABLE_PI_DECODER + if(pi_decoder) + chiaki_session_set_video_sample_cb(&session, chiaki_pi_decoder_video_sample_cb, pi_decoder); + else + { +#endif + chiaki_session_set_video_sample_cb(&session, VideoSampleCb, this); +#if CHIAKI_LIB_ENABLE_PI_DECODER + } +#endif + chiaki_session_set_event_cb(&session, EventCb, this); #if CHIAKI_GUI_ENABLE_SDL_GAMECONTROLLER @@ -107,6 +137,14 @@ StreamSession::~StreamSession() #if CHIAKI_GUI_ENABLE_SETSU setsu_free(setsu); #endif +#if CHIAKI_LIB_ENABLE_PI_DECODER + if(pi_decoder) + { + chiaki_pi_decoder_fini(pi_decoder); + free(pi_decoder); + } +#endif + delete video_decoder; } void StreamSession::Start() @@ -285,7 +323,7 @@ void StreamSession::PushAudioFrame(int16_t *buf, size_t samples_count) void StreamSession::PushVideoSample(uint8_t *buf, size_t buf_size) { - video_decoder.PushFrame(buf, buf_size); + video_decoder->PushFrame(buf, buf_size); } void StreamSession::Event(ChiakiEvent *event) diff --git a/gui/src/streamwindow.cpp b/gui/src/streamwindow.cpp index ea712cf..eae263d 100644 --- a/gui/src/streamwindow.cpp +++ b/gui/src/streamwindow.cpp @@ -47,8 +47,17 @@ void StreamWindow::Init() connect(session, &StreamSession::SessionQuit, this, &StreamWindow::SessionQuit); connect(session, &StreamSession::LoginPINRequested, this, &StreamWindow::LoginPINRequested); - av_widget = new AVOpenGLWidget(session->GetVideoDecoder(), this); - setCentralWidget(av_widget); + if(session->GetVideoDecoder()) + { + av_widget = new AVOpenGLWidget(session->GetVideoDecoder(), this); + setCentralWidget(av_widget); + } + else + { + QWidget *bg_widget = new QWidget(this); + bg_widget->setStyleSheet("background-color: black;"); + setCentralWidget(bg_widget); + } grabKeyboard(); @@ -167,3 +176,34 @@ void StreamWindow::ToggleFullscreen() av_widget->HideMouse(); } } + +void StreamWindow::resizeEvent(QResizeEvent *event) +{ + UpdateVideoTransform(); + QMainWindow::resizeEvent(event); +} + +void StreamWindow::moveEvent(QMoveEvent *event) +{ + UpdateVideoTransform(); + QMainWindow::moveEvent(event); +} + +void StreamWindow::changeEvent(QEvent *event) +{ + if(event->type() == QEvent::ActivationChange) + UpdateVideoTransform(); + QMainWindow::changeEvent(event); +} + +void StreamWindow::UpdateVideoTransform() +{ +#if CHIAKI_LIB_ENABLE_PI_DECODER + ChiakiPiDecoder *pi_decoder = session->GetPiDecoder(); + if(pi_decoder) + { + QRect r = geometry(); + chiaki_pi_decoder_set_params(pi_decoder, r.x(), r.y(), r.width(), r.height(), isActiveWindow()); + } +#endif +} diff --git a/lib/CMakeLists.txt b/lib/CMakeLists.txt index 4d9e98f..5af259f 100644 --- a/lib/CMakeLists.txt +++ b/lib/CMakeLists.txt @@ -73,6 +73,12 @@ set(SOURCE_FILES src/regist.c src/opusdecoder.c) +if(CHIAKI_ENABLE_PI_DECODER) + list(APPEND HEADER_FILES include/chiaki/pidecoder.h) + list(APPEND SOURCE_FILES src/pidecoder.c) +endif() +set(CHIAKI_LIB_ENABLE_PI_DECODER "${CHIAKI_ENABLE_PI_DECODER}") + add_subdirectory(protobuf) set_source_files_properties(${CHIAKI_LIB_PROTO_SOURCE_FILES} ${CHIAKI_LIB_PROTO_HEADER_FILES} PROPERTIES GENERATED TRUE) include_directories("${CHIAKI_LIB_PROTO_INCLUDE_DIR}") @@ -119,6 +125,10 @@ endif() target_link_libraries(chiaki-lib Nanopb::nanopb) target_link_libraries(chiaki-lib Jerasure::Jerasure) +if(CHIAKI_ENABLE_PI_DECODER) + target_link_libraries(chiaki-lib ILClient::ILClient) +endif() + if(CHIAKI_LIB_ENABLE_OPUS) target_link_libraries(chiaki-lib ${Opus_LIBRARIES}) endif() diff --git a/lib/config.h.in b/lib/config.h.in index cd0a5eb..d06ab98 100644 --- a/lib/config.h.in +++ b/lib/config.h.in @@ -4,5 +4,6 @@ #define CHIAKI_CONFIG_H #cmakedefine01 CHIAKI_LIB_ENABLE_OPUS +#cmakedefine01 CHIAKI_LIB_ENABLE_PI_DECODER #endif // CHIAKI_CONFIG_H diff --git a/lib/include/chiaki/pidecoder.h b/lib/include/chiaki/pidecoder.h new file mode 100644 index 0000000..af2b18b --- /dev/null +++ b/lib/include/chiaki/pidecoder.h @@ -0,0 +1,36 @@ +#ifndef CHIAKI_PI_DECODER_H +#define CHIAKI_PI_DECODER_H + +#include +#include + +#include + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct chiaki_pi_decoder_t +{ + ChiakiLog *log; + TUNNEL_T tunnel[2]; + COMPONENT_T *components[3]; + ILCLIENT_T *client; + COMPONENT_T *video_decode; + COMPONENT_T *video_render; + bool port_settings_changed; + bool first_packet; +} ChiakiPiDecoder; + +CHIAKI_EXPORT ChiakiErrorCode chiaki_pi_decoder_init(ChiakiPiDecoder *decoder, ChiakiLog *log); +CHIAKI_EXPORT void chiaki_pi_decoder_fini(ChiakiPiDecoder *decoder); +CHIAKI_EXPORT void chiaki_pi_decoder_set_params(ChiakiPiDecoder *decoder, int x, int y, int w, int h, bool visible); +CHIAKI_EXPORT bool chiaki_pi_decoder_video_sample_cb(uint8_t *buf, size_t buf_size, void *user); + +#ifdef __cplusplus +} +#endif + +#endif // CHIAKI_PI_DECODER_H diff --git a/lib/src/pidecoder.c b/lib/src/pidecoder.c new file mode 100644 index 0000000..72a22f5 --- /dev/null +++ b/lib/src/pidecoder.c @@ -0,0 +1,261 @@ + +#include + +#include + +#include +#include +#include +#include + +#define MAX_DECODE_UNIT_SIZE 262144 + +CHIAKI_EXPORT ChiakiErrorCode chiaki_pi_decoder_init(ChiakiPiDecoder *decoder, ChiakiLog *log) +{ + memset(decoder, 0, sizeof(ChiakiPiDecoder)); + + bcm_host_init(); + + if(!(decoder->client = ilclient_init())) + { + CHIAKI_LOGE(decoder->log, "ilclient_init failed"); + chiaki_pi_decoder_fini(decoder); + return CHIAKI_ERR_UNKNOWN; + } + + if(OMX_Init() != OMX_ErrorNone) + { + CHIAKI_LOGE(decoder->log, "OMX_Init failed"); + chiaki_pi_decoder_fini(decoder); + return CHIAKI_ERR_UNKNOWN; + } + + if(ilclient_create_component(decoder->client, &decoder->video_decode, "video_decode", + ILCLIENT_DISABLE_ALL_PORTS | ILCLIENT_ENABLE_INPUT_BUFFERS) != 0) + { + CHIAKI_LOGE(decoder->log, "ilclient_create_component failed for video_decode"); + chiaki_pi_decoder_fini(decoder); + return CHIAKI_ERR_UNKNOWN; + } + decoder->components[0] = decoder->video_decode; + + if(ilclient_create_component(decoder->client, &decoder->video_render, "video_render", ILCLIENT_DISABLE_ALL_PORTS) != 0) + { + CHIAKI_LOGE(decoder->log, "ilclient_create_component failed for video_render"); + chiaki_pi_decoder_fini(decoder); + return CHIAKI_ERR_UNKNOWN; + } + decoder->components[1] = decoder->video_render; + + set_tunnel(decoder->tunnel, decoder->video_decode, 131, decoder->video_render, 90); + + ilclient_change_component_state(decoder->video_decode, OMX_StateIdle); + + OMX_VIDEO_PARAM_PORTFORMATTYPE format; + memset(&format, 0, sizeof(OMX_VIDEO_PARAM_PORTFORMATTYPE)); + format.nSize = sizeof(OMX_VIDEO_PARAM_PORTFORMATTYPE); + format.nVersion.nVersion = OMX_VERSION; + format.nPortIndex = 130; + format.eCompressionFormat = OMX_VIDEO_CodingAVC; + + OMX_PARAM_DATAUNITTYPE unit; + memset(&unit, 0, sizeof(OMX_PARAM_DATAUNITTYPE)); + unit.nSize = sizeof(OMX_PARAM_DATAUNITTYPE); + unit.nVersion.nVersion = OMX_VERSION; + unit.nPortIndex = 130; + unit.eUnitType = OMX_DataUnitCodedPicture; + unit.eEncapsulationType = OMX_DataEncapsulationElementaryStream; + + if(OMX_SetParameter(ILC_GET_HANDLE(decoder->video_decode), OMX_IndexParamVideoPortFormat, &format) != OMX_ErrorNone + || OMX_SetParameter(ILC_GET_HANDLE(decoder->video_decode), OMX_IndexParamBrcmDataUnit, &unit) != OMX_ErrorNone) + { + CHIAKI_LOGE(decoder->log, "OMX_SetParameter failed for video parameters"); + chiaki_pi_decoder_fini(decoder); + return CHIAKI_ERR_UNKNOWN; + } + + OMX_CONFIG_LATENCYTARGETTYPE latencyTarget; + memset(&latencyTarget, 0, sizeof(OMX_CONFIG_LATENCYTARGETTYPE)); + latencyTarget.nSize = sizeof(OMX_PARAM_PORTDEFINITIONTYPE); + latencyTarget.nVersion.nVersion = OMX_VERSION; + latencyTarget.nPortIndex = 90; + latencyTarget.bEnabled = OMX_TRUE; + latencyTarget.nFilter = 2; + latencyTarget.nTarget = 4000; + latencyTarget.nShift = 3; + latencyTarget.nSpeedFactor = -135; + latencyTarget.nInterFactor = 500; + latencyTarget.nAdjCap = 20; + + if(OMX_SetParameter(ILC_GET_HANDLE(decoder->video_render), OMX_IndexConfigLatencyTarget, &latencyTarget) != OMX_ErrorNone) + { + CHIAKI_LOGE(decoder->log, "OMX_SetParameter failed for render parameters"); + chiaki_pi_decoder_fini(decoder); + return CHIAKI_ERR_UNKNOWN; + } + + OMX_CONFIG_ROTATIONTYPE rotationType; + memset(&rotationType, 0, sizeof(OMX_CONFIG_ROTATIONTYPE)); + rotationType.nSize = sizeof(OMX_CONFIG_ROTATIONTYPE); + rotationType.nVersion.nVersion = OMX_VERSION; + rotationType.nPortIndex = 90; + //rotationType.nRotation = 90; // example + + if(OMX_SetParameter(ILC_GET_HANDLE(decoder->video_render), OMX_IndexConfigCommonRotate, &rotationType) != OMX_ErrorNone) + { + CHIAKI_LOGE(decoder->log, "OMX_SetParameter failed for rotation"); + chiaki_pi_decoder_fini(decoder); + return CHIAKI_ERR_UNKNOWN; + } + + OMX_PARAM_PORTDEFINITIONTYPE port; + + memset(&port, 0, sizeof(OMX_PARAM_PORTDEFINITIONTYPE)); + port.nSize = sizeof(OMX_PARAM_PORTDEFINITIONTYPE); + port.nVersion.nVersion = OMX_VERSION; + port.nPortIndex = 130; + if(OMX_GetParameter(ILC_GET_HANDLE(decoder->video_decode), OMX_IndexParamPortDefinition, &port) != OMX_ErrorNone) + { + CHIAKI_LOGE(decoder->log, "Failed to get decoder port definition\n"); + chiaki_pi_decoder_fini(decoder); + return CHIAKI_ERR_UNKNOWN; + } + + // Increase the buffer size to fit the largest possible frame + port.nBufferSize = MAX_DECODE_UNIT_SIZE; + + if(OMX_SetParameter(ILC_GET_HANDLE(decoder->video_decode), OMX_IndexParamPortDefinition, &port) != OMX_ErrorNone) + { + CHIAKI_LOGE(decoder->log, "OMX_SetParameter failed for port"); + chiaki_pi_decoder_fini(decoder); + return CHIAKI_ERR_UNKNOWN; + } + + if(ilclient_enable_port_buffers(decoder->video_decode, 130, NULL, NULL, NULL) != 0) + { + CHIAKI_LOGE(decoder->log, "ilclient_enable_port_buffers failed"); + chiaki_pi_decoder_fini(decoder); + return CHIAKI_ERR_UNKNOWN; + } + + decoder->port_settings_changed = false; + decoder->first_packet = true; + + ilclient_change_component_state(decoder->video_decode, OMX_StateExecuting); + + CHIAKI_LOGI(decoder->log, "Raspberry Pi Decoder initialized"); + return CHIAKI_ERR_SUCCESS; +} + +CHIAKI_EXPORT void chiaki_pi_decoder_fini(ChiakiPiDecoder *decoder) +{ + if(decoder->video_decode) + { + OMX_BUFFERHEADERTYPE *buf; + if((buf = ilclient_get_input_buffer(decoder->video_decode, 130, 1))) + { + buf->nFilledLen = 0; + buf->nFlags = OMX_BUFFERFLAG_TIME_UNKNOWN | OMX_BUFFERFLAG_EOS; + OMX_EmptyThisBuffer(ILC_GET_HANDLE(decoder->video_decode), buf); + } + + // need to flush the renderer to allow video_decode to disable its input port + ilclient_flush_tunnels(decoder->tunnel, 0); + + ilclient_disable_port_buffers(decoder->video_decode, 130, NULL, NULL, NULL); + + ilclient_disable_tunnel(decoder->tunnel); + ilclient_teardown_tunnels(decoder->tunnel); + + ilclient_state_transition(decoder->components, OMX_StateIdle); + ilclient_state_transition(decoder->components, OMX_StateLoaded); + + ilclient_cleanup_components(decoder->components); + } + + OMX_Deinit(); + if(decoder->client) + ilclient_destroy(decoder->client); +} + +static bool push_buffer(ChiakiPiDecoder *decoder, uint8_t *buf, size_t buf_size) +{ + OMX_BUFFERHEADERTYPE *omx_buf = ilclient_get_input_buffer(decoder->video_decode, 130, 1); + if(!omx_buf) + { + CHIAKI_LOGE(decoder->log, "ilclient_get_input_buffer failed"); + return false; + } + + if(omx_buf->nAllocLen < buf_size) + { + CHIAKI_LOGE(decoder->log, "Buffer from omx is too small for frame"); + return false; + } + + omx_buf->nFilledLen = 0; + omx_buf->nOffset = 0; + omx_buf->nFlags = OMX_BUFFERFLAG_ENDOFFRAME; + if(decoder->first_packet) + { + omx_buf->nFlags |= OMX_BUFFERFLAG_STARTTIME; + decoder->first_packet = false; + } + + memcpy(omx_buf->pBuffer + omx_buf->nFilledLen, buf, buf_size); + omx_buf->nFilledLen += buf_size; + + if(!decoder->port_settings_changed + && ((omx_buf->nFilledLen > 0 && ilclient_remove_event(decoder->video_decode, OMX_EventPortSettingsChanged, 131, 0, 0, 1) == 0) + || (omx_buf->nFilledLen == 0 && ilclient_wait_for_event(decoder->video_decode, OMX_EventPortSettingsChanged, 131, 0, 0, 1, ILCLIENT_EVENT_ERROR | ILCLIENT_PARAMETER_CHANGED, 10000) == 0))) + { + decoder->port_settings_changed = true; + + if(ilclient_setup_tunnel(decoder->tunnel, 0, 0) != 0) + { + CHIAKI_LOGE(decoder->log, "ilclient_setup_tunnel failed"); + return false; + } + + ilclient_change_component_state(decoder->video_render, OMX_StateExecuting); + } + + if(OMX_EmptyThisBuffer(ILC_GET_HANDLE(decoder->video_decode), omx_buf) != OMX_ErrorNone) + { + CHIAKI_LOGE(decoder->log, "OMX_EmptyThisBuffer failed"); + return false; + } + return true; +} + +#define OMX_INIT_STRUCTURE(a) \ + memset(&(a), 0, sizeof(a)); \ + (a).nSize = sizeof(a); \ + (a).nVersion.s.nVersionMajor = OMX_VERSION_MAJOR; \ + (a).nVersion.s.nVersionMinor = OMX_VERSION_MINOR; \ + (a).nVersion.s.nRevision = OMX_VERSION_REVISION; \ + (a).nVersion.s.nStep = OMX_VERSION_STEP + +CHIAKI_EXPORT void chiaki_pi_decoder_set_params(ChiakiPiDecoder *decoder, int x, int y, int w, int h, bool visible) +{ + OMX_CONFIG_DISPLAYREGIONTYPE configDisplay; + OMX_INIT_STRUCTURE(configDisplay); + configDisplay.nPortIndex = 90; + configDisplay.set = (OMX_DISPLAYSETTYPE)(OMX_DISPLAY_SET_NOASPECT | OMX_DISPLAY_SET_MODE | OMX_DISPLAY_SET_FULLSCREEN | OMX_DISPLAY_SET_PIXEL | OMX_DISPLAY_SET_DEST_RECT | OMX_DISPLAY_SET_ALPHA); + configDisplay.mode = OMX_DISPLAY_MODE_LETTERBOX; + configDisplay.fullscreen = OMX_FALSE; + configDisplay.noaspect = OMX_FALSE; + configDisplay.dest_rect.x_offset = x; + configDisplay.dest_rect.y_offset = y; + configDisplay.dest_rect.width = w; + configDisplay.dest_rect.height = h; + configDisplay.alpha = visible ? 255 : 0; + + if(OMX_SetParameter(ILC_GET_HANDLE(decoder->video_render), OMX_IndexConfigDisplayRegion, &configDisplay) != OMX_ErrorNone) + CHIAKI_LOGE(decoder->log, "OMX_SetParameter failed for display params"); +} + +CHIAKI_EXPORT bool chiaki_pi_decoder_video_sample_cb(uint8_t *buf, size_t buf_size, void *user) +{ + return push_buffer(user, buf, buf_size); +} From ea8ebc812e99770e45e08f87e88451ad04b88acd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Wed, 18 Nov 2020 20:06:09 +0100 Subject: [PATCH 070/237] Fix a nice chance for hiep explontation --- lib/src/frameprocessor.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/frameprocessor.c b/lib/src/frameprocessor.c index 17d02e1..6831dea 100644 --- a/lib/src/frameprocessor.c +++ b/lib/src/frameprocessor.c @@ -196,7 +196,7 @@ static ChiakiErrorCode chiaki_frame_processor_fec(ChiakiFrameProcessor *frame_pr ChiakiErrorCode err = chiaki_fec_decode(frame_processor->frame_buf, frame_processor->buf_size_per_unit, frame_processor->buf_stride_per_unit, - frame_processor->units_source_expected, frame_processor->units_fec_received, + frame_processor->units_source_expected, frame_processor->units_fec_expected, erasures, erasures_count); if(err != CHIAKI_ERR_SUCCESS) From ef7a97f6aecb72a1bc9c3872a089a8a6e74675fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Wed, 18 Nov 2020 11:55:26 +0100 Subject: [PATCH 071/237] Fix Congestion in Takion and add Test --- lib/include/chiaki/takion.h | 13 +++++++++--- lib/src/takion.c | 32 ++++++++++++++--------------- test/takion.c | 41 +++++++++++++++++++++++++++++++++++++ 3 files changed, 67 insertions(+), 19 deletions(-) diff --git a/lib/include/chiaki/takion.h b/lib/include/chiaki/takion.h index 566a996..f957948 100644 --- a/lib/include/chiaki/takion.h +++ b/lib/include/chiaki/takion.h @@ -58,8 +58,8 @@ typedef ChiakiErrorCode (*ChiakiTakionAVPacketParse)(ChiakiTakionAVPacket *packe typedef struct chiaki_takion_congestion_packet_t { uint16_t word_0; - uint16_t word_1; - uint16_t word_2; + uint16_t received; + uint16_t lost; } ChiakiTakionCongestionPacket; @@ -167,11 +167,14 @@ CHIAKI_EXPORT void chiaki_takion_close(ChiakiTakion *takion); /** * Must be called from within the Takion thread, i.e. inside the callback! */ -static inline void chiaki_takion_set_crypt(ChiakiTakion *takion, ChiakiGKCrypt *gkcrypt_local, ChiakiGKCrypt *gkcrypt_remote) { +static inline void chiaki_takion_set_crypt(ChiakiTakion *takion, ChiakiGKCrypt *gkcrypt_local, ChiakiGKCrypt *gkcrypt_remote) +{ takion->gkcrypt_local = gkcrypt_local; takion->gkcrypt_remote = gkcrypt_remote; } +CHIAKI_EXPORT ChiakiErrorCode chiaki_takion_packet_mac(ChiakiGKCrypt *crypt, uint8_t *buf, size_t buf_size, uint64_t key_pos, uint8_t *mac_out, uint8_t *mac_old_out); + /** * Get a new key pos and advance by data_size. * @@ -230,6 +233,10 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_takion_v7_av_packet_format_header(uint8_t * CHIAKI_EXPORT ChiakiErrorCode chiaki_takion_v7_av_packet_parse(ChiakiTakionAVPacket *packet, ChiakiKeyState *key_state, uint8_t *buf, size_t buf_size); +#define CHIAKI_TAKION_CONGESTION_PACKET_SIZE 0xf + +CHIAKI_EXPORT void chiaki_takion_format_congestion(uint8_t *buf, ChiakiTakionCongestionPacket *packet, uint64_t key_pos); + #ifdef __cplusplus } #endif diff --git a/lib/src/takion.c b/lib/src/takion.c index dcb26f2..80daf4f 100644 --- a/lib/src/takion.c +++ b/lib/src/takion.c @@ -361,7 +361,7 @@ static ChiakiErrorCode chiaki_takion_packet_read_key_pos(ChiakiTakion *takion, u return CHIAKI_ERR_SUCCESS; } -static ChiakiErrorCode chiaki_takion_packet_mac(ChiakiGKCrypt *crypt, uint8_t *buf, size_t buf_size, uint64_t key_pos, uint8_t *mac_out, uint8_t *mac_old_out) +CHIAKI_EXPORT ChiakiErrorCode chiaki_takion_packet_mac(ChiakiGKCrypt *crypt, uint8_t *buf, size_t buf_size, uint64_t key_pos, uint8_t *mac_out, uint8_t *mac_old_out) { if(buf_size < 1) return CHIAKI_ERR_BUF_TOO_SMALL; @@ -383,17 +383,18 @@ static ChiakiErrorCode chiaki_takion_packet_mac(ChiakiGKCrypt *crypt, uint8_t *b if(crypt) { uint8_t key_pos_tmp[sizeof(uint32_t)]; - if(base_type == TAKION_PACKET_TYPE_CONTROL) + if(base_type == TAKION_PACKET_TYPE_CONTROL || base_type == TAKION_PACKET_TYPE_CONGESTION) { memcpy(key_pos_tmp, buf + key_pos_offset, sizeof(uint32_t)); memset(buf + key_pos_offset, 0, sizeof(uint32_t)); } chiaki_gkcrypt_gmac(crypt, key_pos, buf, buf_size, buf + mac_offset); - if(base_type == TAKION_PACKET_TYPE_CONTROL) + if(base_type == TAKION_PACKET_TYPE_CONTROL || base_type == TAKION_PACKET_TYPE_CONGESTION) memcpy(buf + key_pos_offset, key_pos_tmp, sizeof(uint32_t)); } - memcpy(mac_out, buf + mac_offset, CHIAKI_GKCRYPT_GMAC_SIZE); + if(mac_out) + memcpy(mac_out, buf + mac_offset, CHIAKI_GKCRYPT_GMAC_SIZE); return CHIAKI_ERR_SUCCESS; } @@ -484,23 +485,25 @@ static ChiakiErrorCode chiaki_takion_send_message_data_ack(ChiakiTakion *takion, return chiaki_takion_send(takion, buf, sizeof(buf), key_pos); } -CHIAKI_EXPORT ChiakiErrorCode chiaki_takion_send_congestion(ChiakiTakion *takion, ChiakiTakionCongestionPacket *packet) +CHIAKI_EXPORT void chiaki_takion_format_congestion(uint8_t *buf, ChiakiTakionCongestionPacket *packet, uint64_t key_pos) { - uint8_t buf[0xf]; - memset(buf, 0, sizeof(buf)); buf[0] = TAKION_PACKET_TYPE_CONGESTION; *((chiaki_unaligned_uint16_t *)(buf + 1)) = htons(packet->word_0); - *((chiaki_unaligned_uint16_t *)(buf + 3)) = htons(packet->word_1); - *((chiaki_unaligned_uint16_t *)(buf + 5)) = htons(packet->word_2); + *((chiaki_unaligned_uint16_t *)(buf + 3)) = htons(packet->received); + *((chiaki_unaligned_uint16_t *)(buf + 5)) = htons(packet->lost); + *((chiaki_unaligned_uint32_t *)(buf + 7)) = 0; + *((chiaki_unaligned_uint32_t *)(buf + 0xb)) = htonl((uint32_t)key_pos); +} +CHIAKI_EXPORT ChiakiErrorCode chiaki_takion_send_congestion(ChiakiTakion *takion, ChiakiTakionCongestionPacket *packet) +{ uint64_t key_pos; - ChiakiErrorCode err = chiaki_takion_crypt_advance_key_pos(takion, sizeof(buf), &key_pos); + ChiakiErrorCode err = chiaki_takion_crypt_advance_key_pos(takion, CHIAKI_TAKION_CONGESTION_PACKET_SIZE, &key_pos); if(err != CHIAKI_ERR_SUCCESS) return err; - *((chiaki_unaligned_uint32_t *)(buf + 0xb)) = htonl((uint32_t)key_pos); // TODO: is this correct? shouldn't key_pos be 0 for mac calculation? - - //chiaki_log_hexdump(takion->log, CHIAKI_LOG_DEBUG, buf, sizeof(buf)); + uint8_t buf[CHIAKI_TAKION_CONGESTION_PACKET_SIZE]; + chiaki_takion_format_congestion(buf, packet, key_pos); return chiaki_takion_send(takion, buf, sizeof(buf), key_pos); } @@ -675,9 +678,6 @@ static void *takion_thread_func(void *user) if(chiaki_takion_send_buffer_init(&takion->send_buffer, takion, TAKION_SEND_BUFFER_SIZE) != CHIAKI_ERR_SUCCESS) goto error_reoder_queue; - // TODO ChiakiCongestionControl congestion_control; - // if(chiaki_congestion_control_start(&congestion_control, takion) != CHIAKI_ERR_SUCCESS) - // goto beach; if(takion->cb) { diff --git a/test/takion.c b/test/takion.c index 6bf342f..8cdf02e 100644 --- a/test/takion.c +++ b/test/takion.c @@ -160,7 +160,40 @@ static MunitResult test_takion_send_buffer(const MunitParameter params[], void * #undef nums_count } +static MunitResult test_takion_format_congestion(const MunitParameter params[], void *user) +{ + static const uint8_t handshake_key[] = { 0x54, 0x65, 0x4c, 0x34, 0x5c, 0xac, 0x56, 0xb8, 0xea, 0xe6, 0x15, 0x2a, 0xde, 0x1c, 0xe2, 0xe8 }; + static const uint8_t ecdh_secret[] = { 0x00, 0x34, 0xf8, 0x21, 0xc7, 0xd9, 0xde, 0xa9, 0xe9, 0x11, 0xca, 0x5a, 0xd6, 0x7d, 0x11, 0xce, 0x4f, 0x02, 0xb1, 0xce, 0x1e, 0xe7, 0xc3, 0x8d, 0x54, 0x39, 0xfa, 0x64, 0xe3, 0xdb, 0xd8, 0x0d }; + ChiakiGKCrypt gkcrypt; + ChiakiErrorCode err = chiaki_gkcrypt_init(&gkcrypt, NULL, 0, 2, handshake_key, ecdh_secret); + if(err != CHIAKI_ERR_SUCCESS) + return MUNIT_ERROR; + + ChiakiTakionCongestionPacket packet; + packet.word_0 = 0x42; + packet.received = 26; + packet.lost = 10; + + const uint64_t key_pos = 0x1e5; + + uint8_t buf[CHIAKI_TAKION_CONGESTION_PACKET_SIZE]; + chiaki_takion_format_congestion(buf, &packet, key_pos); + + static const uint8_t buf_expected[] = { 0x05, 0x00, 0x42, 0x00, 0x1a, 0x00, 0x0a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xe5 }; + munit_assert_memory_equal(sizeof(buf), buf, buf_expected); + + err = chiaki_takion_packet_mac(&gkcrypt, buf, sizeof(buf), key_pos, NULL, NULL); + if(err != CHIAKI_ERR_SUCCESS) + return MUNIT_ERROR; + + static const uint8_t buf_expected_mac[] = { 0x05, 0x00, 0x42, 0x00, 0x1a, 0x00, 0x0a, 0x64, 0x8a, 0x7c, 0x74, 0x00, 0x00, 0x01, 0xe5 }; + munit_assert_memory_equal(sizeof(buf), buf, buf_expected_mac); + + chiaki_gkcrypt_fini(&gkcrypt); + + return MUNIT_OK; +} MunitTest tests_takion[] = { { @@ -187,5 +220,13 @@ MunitTest tests_takion[] = { MUNIT_TEST_OPTION_NONE, NULL }, + { + "/format_congestion", + test_takion_format_congestion, + NULL, + NULL, + MUNIT_TEST_OPTION_NONE, + NULL + }, { NULL, NULL, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL } }; From ffb88518356a0fa5dcf685b8b9192970e8d10105 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Wed, 18 Nov 2020 20:15:24 +0100 Subject: [PATCH 072/237] Add Congestion Control --- README.md | 1 - lib/CMakeLists.txt | 2 + lib/include/chiaki/audioreceiver.h | 8 ++- lib/include/chiaki/congestioncontrol.h | 4 +- lib/include/chiaki/frameprocessor.h | 14 +++++ lib/include/chiaki/packetstats.h | 38 +++++++++++++ lib/include/chiaki/session.h | 4 -- lib/include/chiaki/streamconnection.h | 7 +++ lib/include/chiaki/videoreceiver.h | 7 ++- lib/src/audioreceiver.c | 10 +++- lib/src/congestioncontrol.c | 15 +++-- lib/src/frameprocessor.c | 48 ++++++++++++++-- lib/src/packetstats.c | 77 ++++++++++++++++++++++++++ lib/src/session.c | 23 -------- lib/src/streamconnection.c | 59 +++++++++++++++++--- lib/src/videoreceiver.c | 12 ++-- 16 files changed, 269 insertions(+), 60 deletions(-) create mode 100644 lib/include/chiaki/packetstats.h create mode 100644 lib/src/packetstats.c diff --git a/README.md b/README.md index ed1df05..3f170ad 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,6 @@ for Linux, FreeBSD, OpenBSD, Android, macOS, Windows, Nintendo Switch and potent Everything necessary for a full streaming session, including the initial registration and wakeup of the console, is supported. The following features however are yet to be implemented: -* Congestion Control * H264 Error Concealment (FEC and active error recovery however are implemented) * Touchpad support (Triggering the Touchpad Button is currently possible from the keyboard though) * Rumble diff --git a/lib/CMakeLists.txt b/lib/CMakeLists.txt index 5af259f..f7fba38 100644 --- a/lib/CMakeLists.txt +++ b/lib/CMakeLists.txt @@ -21,6 +21,7 @@ set(HEADER_FILES include/chiaki/video.h include/chiaki/videoreceiver.h include/chiaki/frameprocessor.h + include/chiaki/packetstats.h include/chiaki/seqnum.h include/chiaki/discovery.h include/chiaki/congestioncontrol.h @@ -59,6 +60,7 @@ set(SOURCE_FILES src/audioreceiver.c src/videoreceiver.c src/frameprocessor.c + src/packetstats.c src/discovery.c src/congestioncontrol.c src/stoppipe.c diff --git a/lib/include/chiaki/audioreceiver.h b/lib/include/chiaki/audioreceiver.h index 9a85f1d..f499d09 100644 --- a/lib/include/chiaki/audioreceiver.h +++ b/lib/include/chiaki/audioreceiver.h @@ -8,6 +8,7 @@ #include "audio.h" #include "takion.h" #include "thread.h" +#include "packetstats.h" #ifdef __cplusplus extern "C" { @@ -33,19 +34,20 @@ typedef struct chiaki_audio_receiver_t ChiakiMutex mutex; ChiakiSeqNum16 frame_index_prev; bool frame_index_startup; // whether frame_index_prev has definitely not wrapped yet + ChiakiPacketStats *packet_stats; } ChiakiAudioReceiver; -CHIAKI_EXPORT ChiakiErrorCode chiaki_audio_receiver_init(ChiakiAudioReceiver *audio_receiver, struct chiaki_session_t *session); +CHIAKI_EXPORT ChiakiErrorCode chiaki_audio_receiver_init(ChiakiAudioReceiver *audio_receiver, struct chiaki_session_t *session, ChiakiPacketStats *packet_stats); CHIAKI_EXPORT void chiaki_audio_receiver_fini(ChiakiAudioReceiver *audio_receiver); CHIAKI_EXPORT void chiaki_audio_receiver_stream_info(ChiakiAudioReceiver *audio_receiver, ChiakiAudioHeader *audio_header); CHIAKI_EXPORT void chiaki_audio_receiver_av_packet(ChiakiAudioReceiver *audio_receiver, ChiakiTakionAVPacket *packet); -static inline ChiakiAudioReceiver *chiaki_audio_receiver_new(struct chiaki_session_t *session) +static inline ChiakiAudioReceiver *chiaki_audio_receiver_new(struct chiaki_session_t *session, ChiakiPacketStats *packet_stats) { ChiakiAudioReceiver *audio_receiver = CHIAKI_NEW(ChiakiAudioReceiver); if(!audio_receiver) return NULL; - ChiakiErrorCode err = chiaki_audio_receiver_init(audio_receiver, session); + ChiakiErrorCode err = chiaki_audio_receiver_init(audio_receiver, session, packet_stats); if(err != CHIAKI_ERR_SUCCESS) { free(audio_receiver); diff --git a/lib/include/chiaki/congestioncontrol.h b/lib/include/chiaki/congestioncontrol.h index 847052f..c1e183e 100644 --- a/lib/include/chiaki/congestioncontrol.h +++ b/lib/include/chiaki/congestioncontrol.h @@ -5,6 +5,7 @@ #include "takion.h" #include "thread.h" +#include "packetstats.h" #ifdef __cplusplus extern "C" { @@ -13,11 +14,12 @@ extern "C" { typedef struct chiaki_congestion_control_t { ChiakiTakion *takion; + ChiakiPacketStats *stats; ChiakiThread thread; ChiakiBoolPredCond stop_cond; } ChiakiCongestionControl; -CHIAKI_EXPORT ChiakiErrorCode chiaki_congestion_control_start(ChiakiCongestionControl *control, ChiakiTakion *takion); +CHIAKI_EXPORT ChiakiErrorCode chiaki_congestion_control_start(ChiakiCongestionControl *control, ChiakiTakion *takion, ChiakiPacketStats *stats); /** * Stop control and join the thread diff --git a/lib/include/chiaki/frameprocessor.h b/lib/include/chiaki/frameprocessor.h index 65ff97d..78d26f0 100644 --- a/lib/include/chiaki/frameprocessor.h +++ b/lib/include/chiaki/frameprocessor.h @@ -5,6 +5,7 @@ #include "common.h" #include "takion.h" +#include "packetstats.h" #include #include @@ -13,6 +14,16 @@ extern "C" { #endif +typedef struct chiaki_stream_stats_t +{ + uint64_t frames; + uint64_t bytes; +} ChiakiStreamStats; + +CHIAKI_EXPORT void chiaki_stream_stats_reset(ChiakiStreamStats *stats); +CHIAKI_EXPORT void chiaki_stream_stats_frame(ChiakiStreamStats *stats, uint64_t size); +CHIAKI_EXPORT uint64_t chiaki_stream_stats_bitrate(ChiakiStreamStats *stats, uint64_t framerate); + struct chiaki_frame_unit_t; typedef struct chiaki_frame_unit_t ChiakiFrameUnit; @@ -29,6 +40,8 @@ typedef struct chiaki_frame_processor_t unsigned int units_fec_received; ChiakiFrameUnit *unit_slots; size_t unit_slots_size; + bool flushed; // whether we have already flushed the current frame, i.e. are only interested in stats, not data. + ChiakiStreamStats stream_stats; } ChiakiFrameProcessor; typedef enum chiaki_frame_flush_result_t { @@ -41,6 +54,7 @@ typedef enum chiaki_frame_flush_result_t { CHIAKI_EXPORT void chiaki_frame_processor_init(ChiakiFrameProcessor *frame_processor, ChiakiLog *log); CHIAKI_EXPORT void chiaki_frame_processor_fini(ChiakiFrameProcessor *frame_processor); +CHIAKI_EXPORT void chiaki_frame_processor_report_packet_stats(ChiakiFrameProcessor *frame_processor, ChiakiPacketStats *packet_stats); CHIAKI_EXPORT ChiakiErrorCode chiaki_frame_processor_alloc_frame(ChiakiFrameProcessor *frame_processor, ChiakiTakionAVPacket *packet); CHIAKI_EXPORT ChiakiErrorCode chiaki_frame_processor_put_unit(ChiakiFrameProcessor *frame_processor, ChiakiTakionAVPacket *packet); diff --git a/lib/include/chiaki/packetstats.h b/lib/include/chiaki/packetstats.h new file mode 100644 index 0000000..dfe4cb9 --- /dev/null +++ b/lib/include/chiaki/packetstats.h @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL + +#ifndef CHIAKI_PACKETSTATS_H +#define CHIAKI_PACKETSTATS_H + +#include "thread.h" +#include "seqnum.h" + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct chiaki_packet_stats_t +{ + ChiakiMutex mutex; + + // For generations of packets, i.e. where we know the number of expected packets per generation + uint64_t gen_received; + uint64_t gen_lost; + + // For sequential packets, i.e. where packets are identified by a sequence number + ChiakiSeqNum16 seq_min; // sequence number that was max at the last reset + ChiakiSeqNum16 seq_max; // currently maximal sequence number + uint64_t seq_received; // total received packets since the last reset +} ChiakiPacketStats; + +CHIAKI_EXPORT ChiakiErrorCode chiaki_packet_stats_init(ChiakiPacketStats *stats); +CHIAKI_EXPORT void chiaki_packet_stats_fini(ChiakiPacketStats *stats); +CHIAKI_EXPORT void chiaki_packet_stats_reset(ChiakiPacketStats *stats); +CHIAKI_EXPORT void chiaki_packet_stats_push_generation(ChiakiPacketStats *stats, uint64_t received, uint64_t lost); +CHIAKI_EXPORT void chiaki_packet_stats_push_seq(ChiakiPacketStats *stats, ChiakiSeqNum16 seq_num); +CHIAKI_EXPORT void chiaki_packet_stats_get(ChiakiPacketStats *stats, bool reset, uint64_t *received, uint64_t *lost); + +#ifdef __cplusplus +} +#endif + +#endif // CHIAKI_PACKETSTATS_H diff --git a/lib/include/chiaki/session.h b/lib/include/chiaki/session.h index 80c89d3..8b4ae97 100644 --- a/lib/include/chiaki/session.h +++ b/lib/include/chiaki/session.h @@ -12,8 +12,6 @@ #include "takion.h" #include "ecdh.h" #include "audio.h" -#include "audioreceiver.h" -#include "videoreceiver.h" #include "controller.h" #include "stoppipe.h" @@ -199,8 +197,6 @@ typedef struct chiaki_session_t ChiakiLog *log; ChiakiStreamConnection stream_connection; - ChiakiAudioReceiver *audio_receiver; - ChiakiVideoReceiver *video_receiver; ChiakiControllerState controller_state; } ChiakiSession; diff --git a/lib/include/chiaki/streamconnection.h b/lib/include/chiaki/streamconnection.h index cc55f8b..ed080f6 100644 --- a/lib/include/chiaki/streamconnection.h +++ b/lib/include/chiaki/streamconnection.h @@ -8,6 +8,9 @@ #include "log.h" #include "ecdh.h" #include "gkcrypt.h" +#include "audioreceiver.h" +#include "videoreceiver.h" +#include "congestioncontrol.h" #include @@ -26,6 +29,10 @@ typedef struct chiaki_stream_connection_t ChiakiGKCrypt *gkcrypt_local; ChiakiGKCrypt *gkcrypt_remote; + ChiakiPacketStats packet_stats; + ChiakiAudioReceiver *audio_receiver; + ChiakiVideoReceiver *video_receiver; + ChiakiFeedbackSender feedback_sender; /** * whether feedback_sender is initialized diff --git a/lib/include/chiaki/videoreceiver.h b/lib/include/chiaki/videoreceiver.h index e68d5a4..111d64f 100644 --- a/lib/include/chiaki/videoreceiver.h +++ b/lib/include/chiaki/videoreceiver.h @@ -27,9 +27,10 @@ typedef struct chiaki_video_receiver_t int32_t frame_index_prev; // last frame that has been at least partially decoded int32_t frame_index_prev_complete; // last frame that has been completely decoded ChiakiFrameProcessor frame_processor; + ChiakiPacketStats *packet_stats; } ChiakiVideoReceiver; -CHIAKI_EXPORT void chiaki_video_receiver_init(ChiakiVideoReceiver *video_receiver, struct chiaki_session_t *session); +CHIAKI_EXPORT void chiaki_video_receiver_init(ChiakiVideoReceiver *video_receiver, struct chiaki_session_t *session, ChiakiPacketStats *packet_stats); CHIAKI_EXPORT void chiaki_video_receiver_fini(ChiakiVideoReceiver *video_receiver); /** @@ -43,12 +44,12 @@ CHIAKI_EXPORT void chiaki_video_receiver_stream_info(ChiakiVideoReceiver *video_ CHIAKI_EXPORT void chiaki_video_receiver_av_packet(ChiakiVideoReceiver *video_receiver, ChiakiTakionAVPacket *packet); -static inline ChiakiVideoReceiver *chiaki_video_receiver_new(struct chiaki_session_t *session) +static inline ChiakiVideoReceiver *chiaki_video_receiver_new(struct chiaki_session_t *session, ChiakiPacketStats *packet_stats) { ChiakiVideoReceiver *video_receiver = CHIAKI_NEW(ChiakiVideoReceiver); if(!video_receiver) return NULL; - chiaki_video_receiver_init(video_receiver, session); + chiaki_video_receiver_init(video_receiver, session, packet_stats); return video_receiver; } diff --git a/lib/src/audioreceiver.c b/lib/src/audioreceiver.c index 266fde1..88b4758 100644 --- a/lib/src/audioreceiver.c +++ b/lib/src/audioreceiver.c @@ -7,10 +7,11 @@ static void chiaki_audio_receiver_frame(ChiakiAudioReceiver *audio_receiver, ChiakiSeqNum16 frame_index, uint8_t *buf, size_t buf_size); -CHIAKI_EXPORT ChiakiErrorCode chiaki_audio_receiver_init(ChiakiAudioReceiver *audio_receiver, ChiakiSession *session) +CHIAKI_EXPORT ChiakiErrorCode chiaki_audio_receiver_init(ChiakiAudioReceiver *audio_receiver, ChiakiSession *session, ChiakiPacketStats *packet_stats) { audio_receiver->session = session; audio_receiver->log = session->log; + audio_receiver->packet_stats = packet_stats; audio_receiver->frame_index_prev = 0; audio_receiver->frame_index_startup = true; @@ -101,8 +102,11 @@ CHIAKI_EXPORT void chiaki_audio_receiver_av_packet(ChiakiAudioReceiver *audio_re frame_index = packet->frame_index - fec_units_count + fec_index; } - chiaki_audio_receiver_frame(audio_receiver->session->audio_receiver, frame_index, packet->data + unit_size * i, unit_size); + chiaki_audio_receiver_frame(audio_receiver, frame_index, packet->data + unit_size * i, unit_size); } + + if(audio_receiver->packet_stats) + chiaki_packet_stats_push_seq(audio_receiver->packet_stats, packet->frame_index); } static void chiaki_audio_receiver_frame(ChiakiAudioReceiver *audio_receiver, ChiakiSeqNum16 frame_index, uint8_t *buf, size_t buf_size) @@ -118,4 +122,4 @@ static void chiaki_audio_receiver_frame(ChiakiAudioReceiver *audio_receiver, Chi beach: chiaki_mutex_unlock(&audio_receiver->mutex); -} \ No newline at end of file +} diff --git a/lib/src/congestioncontrol.c b/lib/src/congestioncontrol.c index 390363b..227f5bd 100644 --- a/lib/src/congestioncontrol.c +++ b/lib/src/congestioncontrol.c @@ -2,10 +2,8 @@ #include - #define CONGESTION_CONTROL_INTERVAL_MS 200 - static void *congestion_control_thread_func(void *user) { ChiakiCongestionControl *control = user; @@ -20,8 +18,14 @@ static void *congestion_control_thread_func(void *user) if(err != CHIAKI_ERR_TIMEOUT) break; - //CHIAKI_LOGD(control->takion->log, "Sending Congestion Control Packet"); - ChiakiTakionCongestionPacket packet = { 0 }; // TODO: fill with real values + uint64_t received; + uint64_t lost; + chiaki_packet_stats_get(control->stats, true, &received, &lost); + ChiakiTakionCongestionPacket packet = { 0 }; + packet.received = (uint16_t)received; + packet.lost = (uint16_t)lost; + CHIAKI_LOGV(control->takion->log, "Sending Congestion Control Packet, received: %u, lost: %u", + (unsigned int)packet.received, (unsigned int)packet.lost); chiaki_takion_send_congestion(control->takion, &packet); } @@ -29,9 +33,10 @@ static void *congestion_control_thread_func(void *user) return NULL; } -CHIAKI_EXPORT ChiakiErrorCode chiaki_congestion_control_start(ChiakiCongestionControl *control, ChiakiTakion *takion) +CHIAKI_EXPORT ChiakiErrorCode chiaki_congestion_control_start(ChiakiCongestionControl *control, ChiakiTakion *takion, ChiakiPacketStats *stats) { control->takion = takion; + control->stats = stats; ChiakiErrorCode err = chiaki_bool_pred_cond_init(&control->stop_cond); if(err != CHIAKI_ERR_SUCCESS) diff --git a/lib/src/frameprocessor.c b/lib/src/frameprocessor.c index 6831dea..c5de224 100644 --- a/lib/src/frameprocessor.c +++ b/lib/src/frameprocessor.c @@ -13,15 +13,32 @@ #include #endif -#define UNIT_SLOTS_MAX 256 +CHIAKI_EXPORT void chiaki_stream_stats_reset(ChiakiStreamStats *stats) +{ + stats->frames = 0; + stats->bytes = 0; +} +CHIAKI_EXPORT void chiaki_stream_stats_frame(ChiakiStreamStats *stats, uint64_t size) +{ + stats->frames++; + stats->bytes += size; + //float br = (float)chiaki_stream_stats_bitrate(stats, 60) / 1000000.0f; + //CHIAKI_LOGD(NULL, "bitrate: %f", br); +} + +CHIAKI_EXPORT uint64_t chiaki_stream_stats_bitrate(ChiakiStreamStats *stats, uint64_t framerate) +{ + return (stats->bytes * 8 * framerate) / stats->frames; +} + +#define UNIT_SLOTS_MAX 256 struct chiaki_frame_unit_t { size_t data_size; }; - CHIAKI_EXPORT void chiaki_frame_processor_init(ChiakiFrameProcessor *frame_processor, ChiakiLog *log) { frame_processor->log = log; @@ -33,6 +50,8 @@ CHIAKI_EXPORT void chiaki_frame_processor_init(ChiakiFrameProcessor *frame_proce frame_processor->units_fec_expected = 0; frame_processor->unit_slots = NULL; frame_processor->unit_slots_size = 0; + frame_processor->flushed = true; + chiaki_stream_stats_reset(&frame_processor->stream_stats); } CHIAKI_EXPORT void chiaki_frame_processor_fini(ChiakiFrameProcessor *frame_processor) @@ -49,6 +68,7 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_frame_processor_alloc_frame(ChiakiFrameProc return CHIAKI_ERR_INVALID_DATA; } + frame_processor->flushed = false; frame_processor->units_source_expected = packet->units_in_frame_total - packet->units_in_frame_fec; frame_processor->units_fec_expected = packet->units_in_frame_fec; if(frame_processor->units_fec_expected < 1) @@ -151,9 +171,12 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_frame_processor_put_unit(ChiakiFrameProcess } unit->data_size = packet->data_size; - memcpy(frame_processor->frame_buf + packet->unit_index * frame_processor->buf_stride_per_unit, - packet->data, - packet->data_size); + if(!frame_processor->flushed) + { + memcpy(frame_processor->frame_buf + packet->unit_index * frame_processor->buf_stride_per_unit, + packet->data, + packet->data_size); + } if(packet->unit_index < frame_processor->units_source_expected) frame_processor->units_source_received++; @@ -163,6 +186,13 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_frame_processor_put_unit(ChiakiFrameProcess return CHIAKI_ERR_SUCCESS; } +CHIAKI_EXPORT void chiaki_frame_processor_report_packet_stats(ChiakiFrameProcessor *frame_processor, ChiakiPacketStats *packet_stats) +{ + uint64_t received = frame_processor->units_source_received + frame_processor->units_fec_received; + uint64_t expected = frame_processor->units_source_expected + frame_processor->units_fec_expected; + chiaki_packet_stats_push_generation(packet_stats, received, expected - received); +} + static ChiakiErrorCode chiaki_frame_processor_fec(ChiakiFrameProcessor *frame_processor) { CHIAKI_LOGI(frame_processor->log, "Frame Processor received %u+%u / %u+%u units, attempting FEC", @@ -232,9 +262,13 @@ static ChiakiErrorCode chiaki_frame_processor_fec(ChiakiFrameProcessor *frame_pr CHIAKI_EXPORT ChiakiFrameProcessorFlushResult chiaki_frame_processor_flush(ChiakiFrameProcessor *frame_processor, uint8_t **frame, size_t *frame_size) { - if(frame_processor->units_source_expected == 0) + if(frame_processor->units_source_expected == 0 || frame_processor->flushed) return CHIAKI_FRAME_PROCESSOR_FLUSH_RESULT_FAILED; + //CHIAKI_LOGD(NULL, "source: %u, fec: %u", + // frame_processor->units_source_expected, + // frame_processor->units_fec_expected); + ChiakiFrameProcessorFlushResult result = CHIAKI_FRAME_PROCESSOR_FLUSH_RESULT_SUCCESS; if(frame_processor->units_source_received < frame_processor->units_source_expected) { @@ -266,6 +300,8 @@ CHIAKI_EXPORT ChiakiFrameProcessorFlushResult chiaki_frame_processor_flush(Chiak cur += part_size; } + chiaki_stream_stats_frame(&frame_processor->stream_stats, (uint64_t)cur); + *frame = frame_processor->frame_buf; *frame_size = cur; return result; diff --git a/lib/src/packetstats.c b/lib/src/packetstats.c new file mode 100644 index 0000000..dce9df1 --- /dev/null +++ b/lib/src/packetstats.c @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL + +#include +#include + +CHIAKI_EXPORT ChiakiErrorCode chiaki_packet_stats_init(ChiakiPacketStats *stats) +{ + stats->gen_received = 0; + stats->gen_lost = 0; + stats->seq_min = 0; + stats->seq_max = 0; + stats->seq_received = 0; + return chiaki_mutex_init(&stats->mutex, false); +} + +CHIAKI_EXPORT void chiaki_packet_stats_fini(ChiakiPacketStats *stats) +{ + chiaki_mutex_fini(&stats->mutex); +} + +static void reset_stats(ChiakiPacketStats *stats) +{ + stats->gen_received = 0; + stats->gen_lost = 0; + stats->seq_min = stats->seq_max; + stats->seq_received = 0; +} + +CHIAKI_EXPORT void chiaki_packet_stats_reset(ChiakiPacketStats *stats) +{ + chiaki_mutex_lock(&stats->mutex); + reset_stats(stats); + chiaki_mutex_unlock(&stats->mutex); +} + +CHIAKI_EXPORT void chiaki_packet_stats_push_generation(ChiakiPacketStats *stats, uint64_t received, uint64_t lost) +{ + chiaki_mutex_lock(&stats->mutex); + stats->gen_received += received; + stats->gen_lost += lost; + chiaki_mutex_unlock(&stats->mutex); +} + +CHIAKI_EXPORT void chiaki_packet_stats_push_seq(ChiakiPacketStats *stats, ChiakiSeqNum16 seq_num) +{ + stats->seq_received++; + if(chiaki_seq_num_16_gt(seq_num, stats->seq_max)) + stats->seq_max = seq_num; +} + +CHIAKI_EXPORT void chiaki_packet_stats_get(ChiakiPacketStats *stats, bool reset, uint64_t *received, uint64_t *lost) +{ + chiaki_mutex_lock(&stats->mutex); + + // gen + *received = stats->gen_received; + *lost = stats->gen_lost; + + //CHIAKI_LOGD(NULL, "gen received: %llu, lost: %llu", + // (unsigned long long)stats->gen_received, + // (unsigned long long)stats->gen_lost); + + // seq + uint64_t seq_diff = stats->seq_max - stats->seq_min; // overflow on purpose if max < min + uint64_t seq_lost = stats->seq_received > seq_diff ? seq_diff : seq_diff - stats->seq_received; + *received += stats->seq_received; + *lost += seq_lost; + + //CHIAKI_LOGD(NULL, "seq received: %llu, lost: %llu", + // (unsigned long long)stats->seq_received, + // (unsigned long long)seq_lost); + + if(reset) + reset_stats(stats); + chiaki_mutex_unlock(&stats->mutex); +} + diff --git a/lib/src/session.c b/lib/src/session.c index 496893d..a70bae7 100644 --- a/lib/src/session.c +++ b/lib/src/session.c @@ -484,20 +484,6 @@ ctrl_failed: QUIT(quit_ctrl); } - session->audio_receiver = chiaki_audio_receiver_new(session); - if(!session->audio_receiver) - { - CHIAKI_LOGE(session->log, "Session failed to initialize Audio Receiver"); - QUIT(quit_ecdh); - } - - session->video_receiver = chiaki_video_receiver_new(session); - if(!session->video_receiver) - { - CHIAKI_LOGE(session->log, "Session failed to initialize Video Receiver"); - QUIT(quit_audio_receiver); - } - chiaki_mutex_unlock(&session->state_mutex); err = chiaki_stream_connection_run(&session->stream_connection); chiaki_mutex_lock(&session->state_mutex); @@ -518,16 +504,7 @@ ctrl_failed: session->quit_reason = CHIAKI_QUIT_REASON_STOPPED; } - chiaki_video_receiver_free(session->video_receiver); - session->video_receiver = NULL; - chiaki_mutex_unlock(&session->state_mutex); - -quit_audio_receiver: - chiaki_audio_receiver_free(session->audio_receiver); - session->audio_receiver = NULL; - -quit_ecdh: chiaki_ecdh_fini(&session->ecdh); quit_ctrl: diff --git a/lib/src/streamconnection.c b/lib/src/streamconnection.c index 387ffec..f3fde0d 100644 --- a/lib/src/streamconnection.c +++ b/lib/src/streamconnection.c @@ -71,10 +71,17 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_stream_connection_init(ChiakiStreamConnecti if(err != CHIAKI_ERR_SUCCESS) goto error_state_mutex; - err = chiaki_mutex_init(&stream_connection->feedback_sender_mutex, false); + err = chiaki_packet_stats_init(&stream_connection->packet_stats); if(err != CHIAKI_ERR_SUCCESS) goto error_state_cond; + stream_connection->video_receiver = NULL; + stream_connection->audio_receiver = NULL; + + err = chiaki_mutex_init(&stream_connection->feedback_sender_mutex, false); + if(err != CHIAKI_ERR_SUCCESS) + goto error_packet_stats; + stream_connection->state = STATE_IDLE; stream_connection->state_finished = false; stream_connection->state_failed = false; @@ -84,6 +91,8 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_stream_connection_init(ChiakiStreamConnecti return CHIAKI_ERR_SUCCESS; +error_packet_stats: + chiaki_packet_stats_fini(&stream_connection->packet_stats); error_state_cond: chiaki_cond_fini(&stream_connection->state_cond); error_state_mutex: @@ -101,6 +110,8 @@ CHIAKI_EXPORT void chiaki_stream_connection_fini(ChiakiStreamConnection *stream_ free(stream_connection->ecdh_secret); + chiaki_packet_stats_fini(&stream_connection->packet_stats); + chiaki_mutex_fini(&stream_connection->feedback_sender_mutex); chiaki_cond_fini(&stream_connection->state_cond); @@ -145,6 +156,21 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_stream_connection_run(ChiakiStreamConnectio goto quit_label; \ } } while(0) + stream_connection->audio_receiver = chiaki_audio_receiver_new(session, &stream_connection->packet_stats); + if(!stream_connection->audio_receiver) + { + CHIAKI_LOGE(session->log, "StreamConnection failed to initialize Audio Receiver"); + return CHIAKI_ERR_UNKNOWN; + } + + stream_connection->video_receiver = chiaki_video_receiver_new(session, &stream_connection->packet_stats); + if(!stream_connection->video_receiver) + { + CHIAKI_LOGE(session->log, "StreamConnection failed to initialize Video Receiver"); + err = CHIAKI_ERR_UNKNOWN; + goto err_audio_receiver; + } + stream_connection->state = STATE_TAKION_CONNECT; stream_connection->state_finished = false; stream_connection->state_failed = false; @@ -154,7 +180,15 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_stream_connection_run(ChiakiStreamConnectio { CHIAKI_LOGE(session->log, "StreamConnection connect failed"); chiaki_mutex_unlock(&stream_connection->state_mutex); - return err; + goto err_video_receiver; + } + + ChiakiCongestionControl congestion_control; + err = chiaki_congestion_control_start(&congestion_control, &stream_connection->takion, &stream_connection->packet_stats); + if(err != CHIAKI_ERR_SUCCESS) + { + CHIAKI_LOGE(session->log, "StreamConnection failed to start Congestion Control"); + goto close_takion; } err = chiaki_cond_timedwait_pred(&stream_connection->state_cond, &stream_connection->state_mutex, EXPECT_TIMEOUT_MS, state_finished_cond_check, stream_connection); @@ -163,7 +197,7 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_stream_connection_run(ChiakiStreamConnectio if(err != CHIAKI_ERR_SUCCESS) { CHIAKI_LOGE(session->log, "StreamConnection Takion connect failed"); - goto close_takion; + goto err_congestion_control; } CHIAKI_LOGI(session->log, "StreamConnection sending big"); @@ -273,12 +307,23 @@ disconnect: err = CHIAKI_ERR_DISCONNECTED; } +err_congestion_control: + chiaki_congestion_control_stop(&congestion_control); + close_takion: chiaki_mutex_unlock(&stream_connection->state_mutex); chiaki_takion_close(&stream_connection->takion); CHIAKI_LOGI(session->log, "StreamConnection closed takion"); +err_video_receiver: + chiaki_video_receiver_free(stream_connection->video_receiver); + stream_connection->video_receiver = NULL; + +err_audio_receiver: + chiaki_audio_receiver_free(stream_connection->audio_receiver); + stream_connection->audio_receiver = NULL; + return err; } @@ -613,9 +658,9 @@ static void stream_connection_takion_data_expect_streaminfo(ChiakiStreamConnecti ChiakiAudioHeader audio_header_s; chiaki_audio_header_load(&audio_header_s, audio_header); - chiaki_audio_receiver_stream_info(stream_connection->session->audio_receiver, &audio_header_s); + chiaki_audio_receiver_stream_info(stream_connection->audio_receiver, &audio_header_s); - chiaki_video_receiver_stream_info(stream_connection->session->video_receiver, + chiaki_video_receiver_stream_info(stream_connection->video_receiver, decode_resolutions_context.video_profiles, decode_resolutions_context.video_profiles_count); @@ -794,9 +839,9 @@ static void stream_connection_takion_av(ChiakiStreamConnection *stream_connectio chiaki_gkcrypt_decrypt(stream_connection->gkcrypt_remote, packet->key_pos + CHIAKI_GKCRYPT_BLOCK_SIZE, packet->data, packet->data_size); if(packet->is_video) - chiaki_video_receiver_av_packet(stream_connection->session->video_receiver, packet); + chiaki_video_receiver_av_packet(stream_connection->video_receiver, packet); else - chiaki_audio_receiver_av_packet(stream_connection->session->audio_receiver, packet); + chiaki_audio_receiver_av_packet(stream_connection->audio_receiver, packet); } diff --git a/lib/src/videoreceiver.c b/lib/src/videoreceiver.c index 48cd8bc..4238bd6 100644 --- a/lib/src/videoreceiver.c +++ b/lib/src/videoreceiver.c @@ -7,7 +7,7 @@ static ChiakiErrorCode chiaki_video_receiver_flush_frame(ChiakiVideoReceiver *video_receiver); -CHIAKI_EXPORT void chiaki_video_receiver_init(ChiakiVideoReceiver *video_receiver, struct chiaki_session_t *session) +CHIAKI_EXPORT void chiaki_video_receiver_init(ChiakiVideoReceiver *video_receiver, struct chiaki_session_t *session, ChiakiPacketStats *packet_stats) { video_receiver->session = session; video_receiver->log = session->log; @@ -19,6 +19,7 @@ CHIAKI_EXPORT void chiaki_video_receiver_init(ChiakiVideoReceiver *video_receive video_receiver->frame_index_prev = -1; chiaki_frame_processor_init(&video_receiver->frame_processor, video_receiver->log); + video_receiver->packet_stats = packet_stats; } CHIAKI_EXPORT void chiaki_video_receiver_fini(ChiakiVideoReceiver *video_receiver) @@ -81,6 +82,9 @@ CHIAKI_EXPORT void chiaki_video_receiver_av_packet(ChiakiVideoReceiver *video_re if(video_receiver->frame_index_cur < 0 || chiaki_seq_num_16_gt(frame_index, (ChiakiSeqNum16)video_receiver->frame_index_cur)) { + if(video_receiver->packet_stats) + chiaki_frame_processor_report_packet_stats(&video_receiver->frame_processor, video_receiver->packet_stats); + // last frame not flushed yet? if(video_receiver->frame_index_cur >= 0 && video_receiver->frame_index_prev != video_receiver->frame_index_cur) chiaki_video_receiver_flush_frame(video_receiver); @@ -97,11 +101,11 @@ CHIAKI_EXPORT void chiaki_video_receiver_av_packet(ChiakiVideoReceiver *video_re chiaki_frame_processor_alloc_frame(&video_receiver->frame_processor, packet); } + chiaki_frame_processor_put_unit(&video_receiver->frame_processor, packet); + // if we are currently building up a frame if(video_receiver->frame_index_cur != video_receiver->frame_index_prev) { - chiaki_frame_processor_put_unit(&video_receiver->frame_processor, packet); - // if we already have enough for the whole frame, flush it already if(chiaki_frame_processor_flush_possible(&video_receiver->frame_processor)) chiaki_video_receiver_flush_frame(video_receiver); @@ -146,4 +150,4 @@ static ChiakiErrorCode chiaki_video_receiver_flush_frame(ChiakiVideoReceiver *vi video_receiver->frame_index_prev_complete = video_receiver->frame_index_cur; return CHIAKI_ERR_SUCCESS; -} \ No newline at end of file +} From a11341f448722cee6c90eae543ed9edb8710bd03 Mon Sep 17 00:00:00 2001 From: Blueroom VR Date: Fri, 20 Nov 2020 04:14:45 -0800 Subject: [PATCH 073/237] Add Audio Device Selection to GUI (Fix #376) (#377) --- gui/CMakeLists.txt | 4 ++-- gui/include/settings.h | 4 ++++ gui/include/settingsdialog.h | 2 ++ gui/include/streamsession.h | 2 ++ gui/src/settings.cpp | 10 ++++++++++ gui/src/settingsdialog.cpp | 37 ++++++++++++++++++++++++++++++++++++ gui/src/streamsession.cpp | 18 ++++++++++++++++-- 7 files changed, 73 insertions(+), 4 deletions(-) diff --git a/gui/CMakeLists.txt b/gui/CMakeLists.txt index 7072b83..23f1b97 100644 --- a/gui/CMakeLists.txt +++ b/gui/CMakeLists.txt @@ -2,7 +2,7 @@ set(CMAKE_INCLUDE_CURRENT_DIR ON) set(CMAKE_AUTOMOC ON) set(CMAKE_AUTORCC ON) -find_package(Qt5 REQUIRED COMPONENTS Core Widgets Gui Multimedia OpenGL Svg) +find_package(Qt5 REQUIRED COMPONENTS Core Widgets Gui Concurrent Multimedia OpenGL Svg) if(APPLE) find_package(Qt5 REQUIRED COMPONENTS MacExtras) endif() @@ -74,7 +74,7 @@ if(CHIAKI_ENABLE_CLI) endif() target_link_libraries(chiaki FFMPEG::avcodec FFMPEG::avutil) -target_link_libraries(chiaki Qt5::Core Qt5::Widgets Qt5::Gui Qt5::Multimedia Qt5::OpenGL Qt5::Svg) +target_link_libraries(chiaki Qt5::Core Qt5::Widgets Qt5::Gui Qt5::Concurrent Qt5::Multimedia Qt5::OpenGL Qt5::Svg) if(APPLE) target_link_libraries(chiaki Qt5::MacExtras) target_compile_definitions(chiaki PRIVATE CHIAKI_GUI_ENABLE_QT_MACEXTRAS) diff --git a/gui/include/settings.h b/gui/include/settings.h index 0d5975a..6b00591 100644 --- a/gui/include/settings.h +++ b/gui/include/settings.h @@ -9,6 +9,7 @@ #include "videodecoder.h" #include +#include enum class ControllerButtonExt { @@ -93,6 +94,9 @@ class Settings : public QObject */ unsigned int GetAudioBufferSize() const; void SetAudioBufferSize(unsigned int size); + + QString GetAudioOutDevice() const; + void SetAudioOutDevice(QString device_name); ChiakiConnectVideoProfile GetVideoProfile(); diff --git a/gui/include/settingsdialog.h b/gui/include/settingsdialog.h index b0b529e..27b4ee8 100644 --- a/gui/include/settingsdialog.h +++ b/gui/include/settingsdialog.h @@ -25,6 +25,7 @@ class SettingsDialog : public QDialog QComboBox *fps_combo_box; QLineEdit *bitrate_edit; QLineEdit *audio_buffer_size_edit; + QComboBox *audio_device_combo_box; QCheckBox *pi_decoder_check_box; QComboBox *hardware_decode_combo_box; @@ -41,6 +42,7 @@ class SettingsDialog : public QDialog void FPSSelected(); void BitrateEdited(); void AudioBufferSizeEdited(); + void AudioOutputSelected(); void HardwareDecodeEngineSelected(); void UpdateHardwareDecodeEngineComboBox(); diff --git a/gui/include/streamsession.h b/gui/include/streamsession.h index 2d5cb32..01becf5 100644 --- a/gui/include/streamsession.h +++ b/gui/include/streamsession.h @@ -42,6 +42,7 @@ struct StreamSessionConnectInfo QMap key_map; Decoder decoder; HardwareDecodeEngine hw_decode_engine; + QString audio_out_device; uint32_t log_level_mask; QString log_file; QString host; @@ -81,6 +82,7 @@ class StreamSession : public QObject ChiakiPiDecoder *pi_decoder; #endif + QAudioDeviceInfo audio_out_device_info; unsigned int audio_buffer_size; QAudioOutput *audio_output; QIODevice *audio_io; diff --git a/gui/src/settings.cpp b/gui/src/settings.cpp index bedce8d..18867f1 100644 --- a/gui/src/settings.cpp +++ b/gui/src/settings.cpp @@ -130,6 +130,16 @@ unsigned int Settings::GetAudioBufferSize() const return v ? v : GetAudioBufferSizeDefault(); } +QString Settings::GetAudioOutDevice() const +{ + return settings.value("settings/audio_out_device").toString(); +} + +void Settings::SetAudioOutDevice(QString device_name) +{ + settings.setValue("settings/audio_out_device", device_name); +} + void Settings::SetAudioBufferSize(unsigned int size) { settings.setValue("settings/audio_buffer_size", size); diff --git a/gui/src/settingsdialog.cpp b/gui/src/settingsdialog.cpp index 2e96b83..2bae786 100644 --- a/gui/src/settingsdialog.cpp +++ b/gui/src/settingsdialog.cpp @@ -19,6 +19,8 @@ #include #include #include +#include +#include #include @@ -151,6 +153,36 @@ SettingsDialog::SettingsDialog(Settings *settings, QWidget *parent) : QDialog(pa audio_buffer_size_edit->setPlaceholderText(tr("Default (%1)").arg(settings->GetAudioBufferSizeDefault())); connect(audio_buffer_size_edit, &QLineEdit::textEdited, this, &SettingsDialog::AudioBufferSizeEdited); + audio_device_combo_box = new QComboBox(this); + audio_device_combo_box->addItem(tr("Auto")); + auto current_audio_device = settings->GetAudioOutDevice(); + if(!current_audio_device.isEmpty()) + { + // temporarily add the selected device before async fetching is done + audio_device_combo_box->addItem(current_audio_device, current_audio_device); + audio_device_combo_box->setCurrentIndex(1); + } + connect(audio_device_combo_box, QOverload::of(&QComboBox::activated), this, [this](){ + this->settings->SetAudioOutDevice(audio_device_combo_box->currentData().toString()); + }); + + // do this async because it's slow, assuming availableDevices() is thread-safe + auto audio_devices_future = QtConcurrent::run([]() { + return QAudioDeviceInfo::availableDevices(QAudio::AudioOutput); + }); + auto audio_devices_future_watcher = new QFutureWatcher>(this); + connect(audio_devices_future_watcher, &QFutureWatcher>::finished, this, [this, audio_devices_future_watcher, settings]() { + auto available_devices = audio_devices_future_watcher->result(); + while(audio_device_combo_box->count() > 1) // remove all but "Auto" + audio_device_combo_box->removeItem(1); + for (QAudioDeviceInfo di : available_devices) + audio_device_combo_box->addItem(di.deviceName(), di.deviceName()); + int audio_out_device_index = audio_device_combo_box->findData(settings->GetAudioOutDevice()); + audio_device_combo_box->setCurrentIndex(audio_out_device_index < 0 ? 0 : audio_out_device_index); + }); + audio_devices_future_watcher->setFuture(audio_devices_future); + general_layout->addRow(tr("Audio Output Device:"), audio_device_combo_box); + // Decode Settings auto decode_settings = new QGroupBox(tr("Decode Settings")); @@ -291,6 +323,11 @@ void SettingsDialog::AudioBufferSizeEdited() settings->SetAudioBufferSize(audio_buffer_size_edit->text().toUInt()); } +void SettingsDialog::AudioOutputSelected() +{ + settings->SetAudioOutDevice(audio_device_combo_box->currentText()); +} + void SettingsDialog::HardwareDecodeEngineSelected() { settings->SetHardwareDecodeEngine((HardwareDecodeEngine)hardware_decode_combo_box->currentData().toInt()); diff --git a/gui/src/streamsession.cpp b/gui/src/streamsession.cpp index bb67f91..d8f5147 100644 --- a/gui/src/streamsession.cpp +++ b/gui/src/streamsession.cpp @@ -20,6 +20,7 @@ StreamSessionConnectInfo::StreamSessionConnectInfo(Settings *settings, QString h key_map = settings->GetControllerMappingForDecoding(); decoder = settings->GetDecoder(); hw_decode_engine = settings->GetHardwareDecodeEngine(); + audio_out_device = settings->GetAudioOutDevice(); log_level_mask = settings->GetLogLevelMask(); log_file = CreateLogFilename(); video_profile = settings->GetVideoProfile(); @@ -67,6 +68,19 @@ StreamSession::StreamSession(const StreamSessionConnectInfo &connect_info, QObje } #endif + audio_out_device_info = QAudioDeviceInfo::defaultOutputDevice(); + if(!connect_info.audio_out_device.isEmpty()) + { + for(QAudioDeviceInfo di : QAudioDeviceInfo::availableDevices(QAudio::AudioOutput)) + { + if(di.deviceName() == connect_info.audio_out_device) + { + audio_out_device_info = di; + break; + } + } + } + chiaki_opus_decoder_init(&opus_decoder, log.GetChiakiLog()); audio_buffer_size = connect_info.audio_buffer_size; @@ -296,7 +310,7 @@ void StreamSession::InitAudio(unsigned int channels, unsigned int rate) audio_format.setCodec("audio/pcm"); audio_format.setSampleType(QAudioFormat::SignedInt); - QAudioDeviceInfo audio_device_info(QAudioDeviceInfo::defaultOutputDevice()); + QAudioDeviceInfo audio_device_info = audio_out_device_info; if(!audio_device_info.isFormatSupported(audio_format)) { CHIAKI_LOGE(log.GetChiakiLog(), "Audio Format with %u channels @ %u Hz not supported by Audio Device %s", @@ -305,7 +319,7 @@ void StreamSession::InitAudio(unsigned int channels, unsigned int rate) return; } - audio_output = new QAudioOutput(audio_format, this); + audio_output = new QAudioOutput(audio_device_info, audio_format, this); audio_output->setBufferSize(audio_buffer_size); audio_io = audio_output->start(); From 01b0f30c65ba0221818f66f375a57a4810dd89ce Mon Sep 17 00:00:00 2001 From: Bartosz Fenski Date: Tue, 24 Nov 2020 15:33:57 +0100 Subject: [PATCH 074/237] Re-add AppStream (#381) --- gui/CMakeLists.txt | 1 + gui/re.chiaki.Chiaki.appdata.xml | 25 +++++++++++++++++++++++++ 2 files changed, 26 insertions(+) create mode 100644 gui/re.chiaki.Chiaki.appdata.xml diff --git a/gui/CMakeLists.txt b/gui/CMakeLists.txt index 23f1b97..2932883 100644 --- a/gui/CMakeLists.txt +++ b/gui/CMakeLists.txt @@ -103,3 +103,4 @@ install(TARGETS chiaki BUNDLE DESTINATION bin) install(FILES chiaki.desktop DESTINATION share/applications) install(FILES chiaki.png DESTINATION share/icons/hicolor/512x512/apps) +install(FILES re.chiaki.Chiaki.appdata.xml DESTINATION share/metainfo) diff --git a/gui/re.chiaki.Chiaki.appdata.xml b/gui/re.chiaki.Chiaki.appdata.xml new file mode 100644 index 0000000..a0c6f07 --- /dev/null +++ b/gui/re.chiaki.Chiaki.appdata.xml @@ -0,0 +1,25 @@ + + + + re.chiaki.Chiaki + chiaki.desktop + Chiaki + Free and Open Source Client for PlayStation 4 Remote Play + CC0-1.0 + GPL-3.0 + Florian Märkl + https://github.com/thestr4ng3r/chiaki + https://github.com/thestr4ng3r/chiaki/blob/master/README.md#usage + +

+ Chiaki is a Free and Open Source Client for PlayStation 4 Remote Play. It can be used to play in real time on a PlayStation 4 as long as there is a network connection. +

+
+ + + https://raw.githubusercontent.com/thestr4ng3r/chiaki/master/assets/screenshot.png + + + chiaki@metallic.software + +
From cfb2a6461121f824cfaeffaaec4e45f66fe2fa4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Wed, 25 Nov 2020 14:04:08 +0100 Subject: [PATCH 075/237] Add qt5-concurrent to FreeBSD Dependencies (#385) --- .builds/freebsd.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.builds/freebsd.yml b/.builds/freebsd.yml index 01b7fdd..404f51b 100644 --- a/.builds/freebsd.yml +++ b/.builds/freebsd.yml @@ -14,6 +14,7 @@ packages: - qt5-buildtools - qt5-gui - qt5-widgets + - qt5-concurrent - qt5-svg - qt5-opengl - qt5-multimedia From 1193c23433674663a94609b40dc18081d39065db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Sun, 29 Nov 2020 13:26:57 +0100 Subject: [PATCH 076/237] Set C++11 --- CMakeLists.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index 1814800..4e49f8b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -64,6 +64,8 @@ if(CHIAKI_ENABLE_SWITCH) # use symbolic links to load font .. add_definitions(-DBOREALIS_RESOURCES="romfs:/") endif() +else() + set(CMAKE_CXX_STANDARD 11) endif() if(CHIAKI_USE_SYSTEM_JERASURE) From 777c3a649a7bfb0108bb5610d381c8679e2b75be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Sun, 29 Nov 2020 15:06:46 +0100 Subject: [PATCH 077/237] Update Android SDK --- android/app/build.gradle | 6 +++--- .../main/java/com/metallic/chiaki/session/StreamSession.kt | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 22dc881..585b3c1 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -18,12 +18,12 @@ def chiakiVersion = "$chiakiVersionMajor.$chiakiVersionMinor.$chiakiVersionPatch println("Determined Chiaki Version: $chiakiVersion") android { - compileSdkVersion 29 - buildToolsVersion "29.0.2" + compileSdkVersion 30 + buildToolsVersion "30.0.2" defaultConfig { applicationId "com.metallic.chiaki" minSdkVersion 21 - targetSdkVersion 29 + targetSdkVersion 30 versionCode 7 versionName chiakiVersion externalNativeBuild { diff --git a/android/app/src/main/java/com/metallic/chiaki/session/StreamSession.kt b/android/app/src/main/java/com/metallic/chiaki/session/StreamSession.kt index 8c54545..f76d37f 100644 --- a/android/app/src/main/java/com/metallic/chiaki/session/StreamSession.kt +++ b/android/app/src/main/java/com/metallic/chiaki/session/StreamSession.kt @@ -112,7 +112,7 @@ class StreamSession(val connectInfo: ConnectInfo, val logManager: LogManager, va val surfaceTexture = surfaceTexture if(surfaceTexture != null) - textureView.surfaceTexture = surfaceTexture + textureView.setSurfaceTexture(surfaceTexture) } fun setLoginPin(pin: String) From 70725f7418484bd66bdbf386e2f9379a03ed61ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Mon, 30 Nov 2020 18:15:19 +0100 Subject: [PATCH 078/237] Add Android Job on Sourcehut (#390) --- .builds/android.yml | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 .builds/android.yml diff --git a/.builds/android.yml b/.builds/android.yml new file mode 100644 index 0000000..9398064 --- /dev/null +++ b/.builds/android.yml @@ -0,0 +1,28 @@ +image: alpine/latest + +packages: +- docker + +sources: +- https://github.com/thestr4ng3r/chiaki.git + +artifacts: +- Chiaki.apk +- Chiaki.aab + +secrets: +- 163950ff-2ac4-423d-a280-d2d9edbef000 +- f4bce41f-f96b-4633-80d8-0ff5dd74dc2a + +tasks: +- build: | + cp -v ~/chiaki-local.properties chiaki/android/local.properties || echo "Secrets not available" + sudo service docker start + timeout 15 sh -c "until sudo docker info; do sleep 0.5; done" + sudo docker run \ + -v /home/build:/home/build \ + -u $(id -u):$(id -g) \ + thestr4ng3r/android:f064ea6 \ + /bin/bash -c "cd /home/build/chiaki/android && ./gradlew assembleRelease bundleRelease" + cp chiaki/android/app/build/outputs/apk/release/app-release*.apk Chiaki.apk + cp chiaki/android/app/build/outputs/bundle/release/app-release*.aab Chiaki.aab From c8b017c7d580e95e763254c08565f0529fd00c3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Mon, 30 Nov 2020 18:17:48 +0100 Subject: [PATCH 079/237] Use Latest Alpine --- .builds/alpine.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.builds/alpine.yml b/.builds/alpine.yml index 88cb3ad..d6d394e 100644 --- a/.builds/alpine.yml +++ b/.builds/alpine.yml @@ -1,5 +1,5 @@ -image: alpine/edge +image: alpine/latest sources: - https://github.com/thestr4ng3r/chiaki.git @@ -14,6 +14,7 @@ packages: - qt5-qtmultimedia-dev - ffmpeg-dev - sdl2-dev + - sdl2-static # this is gone on alpine edge so might be necessary to remove later tasks: - build: | From ea4ff606f02e809c8468ef67f4b175a211104131 Mon Sep 17 00:00:00 2001 From: Blueroom VR Date: Tue, 1 Dec 2020 12:52:46 -0800 Subject: [PATCH 080/237] Ignore IMU-only Controllers in GUI (#387) --- gui/src/controllermanager.cpp | 76 +++++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/gui/src/controllermanager.cpp b/gui/src/controllermanager.cpp index b0b55a1..4f0e5f4 100644 --- a/gui/src/controllermanager.cpp +++ b/gui/src/controllermanager.cpp @@ -11,6 +11,64 @@ #include #endif +static QSet chiaki_motion_controller_guids({ + // Sony on Linux + "03000000341a00003608000011010000", + "030000004c0500006802000010010000", + "030000004c0500006802000010810000", + "030000004c0500006802000011010000", + "030000004c0500006802000011810000", + "030000006f0e00001402000011010000", + "030000008f0e00000300000010010000", + "050000004c0500006802000000010000", + "050000004c0500006802000000800000", + "050000004c0500006802000000810000", + "05000000504c415953544154494f4e00", + "060000004c0500006802000000010000", + "030000004c050000a00b000011010000", + "030000004c050000a00b000011810000", + "030000004c050000c405000011010000", + "030000004c050000c405000011810000", + "030000004c050000cc09000000010000", + "030000004c050000cc09000011010000", + "030000004c050000cc09000011810000", + "03000000c01100000140000011010000", + "050000004c050000c405000000010000", + "050000004c050000c405000000810000", + "050000004c050000c405000001800000", + "050000004c050000cc09000000010000", + "050000004c050000cc09000000810000", + "050000004c050000cc09000001800000", + // Sony on iOS + "050000004c050000cc090000df070000", + // Sony on Android + "050000004c05000068020000dfff3f00", + "030000004c050000cc09000000006800", + "050000004c050000c4050000fffe3f00", + "050000004c050000cc090000fffe3f00", + "050000004c050000cc090000ffff3f00", + "35643031303033326130316330353564", + // Sony on Mac OSx + "030000004c050000cc09000000000000", + "030000004c0500006802000000000000", + "030000004c0500006802000000010000", + "030000004c050000a00b000000010000", + "030000004c050000c405000000000000", + "030000004c050000c405000000010000", + "030000004c050000cc09000000010000", + "03000000c01100000140000000010000", + // Sony on Windows + "030000004c050000a00b000000000000", + "030000004c050000c405000000000000", + "030000004c050000cc09000000000000", + "03000000250900000500000000000000", + "030000004c0500006802000000000000", + "03000000632500007505000000000000", + "03000000888800000803000000000000", + "030000008f0e00001431000000000000", +}); + + static ControllerManager *instance = nullptr; #define UPDATE_INTERVAL_MS 4 @@ -56,6 +114,24 @@ void ControllerManager::UpdateAvailableControllers() { if(!SDL_IsGameController(i)) continue; + + // We'll try to identify pads with Motion Control + SDL_JoystickGUID guid = SDL_JoystickGetGUID(SDL_JoystickOpen(i)); + char guid_str[256]; + SDL_JoystickGetGUIDString(guid, guid_str, sizeof(guid_str)); + + if(chiaki_motion_controller_guids.contains(guid_str)) + { + SDL_Joystick *joy = SDL_JoystickOpen(i); + if(joy) + { + bool no_buttons = SDL_JoystickNumButtons(joy) == 0; + SDL_JoystickClose(joy); + if(no_buttons) + continue; + } + } + current_controllers.insert(SDL_JoystickGetDeviceInstanceID(i)); } From a12f130dc62772ce9e8235016c6737faabe51eb6 Mon Sep 17 00:00:00 2001 From: Blueroom VR Date: Wed, 2 Dec 2020 09:41:29 -0800 Subject: [PATCH 081/237] Add Double Click for Fullscreen in GUI (#391) --- gui/include/streamwindow.h | 1 + gui/src/streamwindow.cpp | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/gui/include/streamwindow.h b/gui/include/streamwindow.h index 5476672..78bb530 100644 --- a/gui/include/streamwindow.h +++ b/gui/include/streamwindow.h @@ -33,6 +33,7 @@ class StreamWindow: public QMainWindow void closeEvent(QCloseEvent *event) override; void mousePressEvent(QMouseEvent *event) override; void mouseReleaseEvent(QMouseEvent *event) override; + void mouseDoubleClickEvent(QMouseEvent *event) override; void resizeEvent(QResizeEvent *event) override; void moveEvent(QMoveEvent *event) override; void changeEvent(QEvent *event) override; diff --git a/gui/src/streamwindow.cpp b/gui/src/streamwindow.cpp index eae263d..e04feda 100644 --- a/gui/src/streamwindow.cpp +++ b/gui/src/streamwindow.cpp @@ -96,6 +96,13 @@ void StreamWindow::mouseReleaseEvent(QMouseEvent *event) session->HandleMouseEvent(event); } +void StreamWindow::mouseDoubleClickEvent(QMouseEvent *event) +{ + ToggleFullscreen(); + + QMainWindow::mouseDoubleClickEvent(event); +} + void StreamWindow::closeEvent(QCloseEvent *event) { if(session) From fc5306cfdd35a22a014ae258fcb3c94a6f8b5446 Mon Sep 17 00:00:00 2001 From: Blueroom VR Date: Wed, 2 Dec 2020 11:27:43 -0800 Subject: [PATCH 082/237] Move Audio Settings above About in GUI (#392) --- gui/src/settingsdialog.cpp | 60 +++++++++++++++++++------------------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/gui/src/settingsdialog.cpp b/gui/src/settingsdialog.cpp index 2bae786..a76170e 100644 --- a/gui/src/settingsdialog.cpp +++ b/gui/src/settingsdialog.cpp @@ -90,6 +90,36 @@ SettingsDialog::SettingsDialog(Settings *settings, QWidget *parent) : QDialog(pa connect(disconnect_action_combo_box, SIGNAL(currentIndexChanged(int)), this, SLOT(DisconnectActionSelected())); general_layout->addRow(tr("Action on Disconnect:"), disconnect_action_combo_box); + + audio_device_combo_box = new QComboBox(this); + audio_device_combo_box->addItem(tr("Auto")); + auto current_audio_device = settings->GetAudioOutDevice(); + if(!current_audio_device.isEmpty()) + { + // temporarily add the selected device before async fetching is done + audio_device_combo_box->addItem(current_audio_device, current_audio_device); + audio_device_combo_box->setCurrentIndex(1); + } + connect(audio_device_combo_box, QOverload::of(&QComboBox::activated), this, [this](){ + this->settings->SetAudioOutDevice(audio_device_combo_box->currentData().toString()); + }); + + // do this async because it's slow, assuming availableDevices() is thread-safe + auto audio_devices_future = QtConcurrent::run([]() { + return QAudioDeviceInfo::availableDevices(QAudio::AudioOutput); + }); + auto audio_devices_future_watcher = new QFutureWatcher>(this); + connect(audio_devices_future_watcher, &QFutureWatcher>::finished, this, [this, audio_devices_future_watcher, settings]() { + auto available_devices = audio_devices_future_watcher->result(); + while(audio_device_combo_box->count() > 1) // remove all but "Auto" + audio_device_combo_box->removeItem(1); + for (QAudioDeviceInfo di : available_devices) + audio_device_combo_box->addItem(di.deviceName(), di.deviceName()); + int audio_out_device_index = audio_device_combo_box->findData(settings->GetAudioOutDevice()); + audio_device_combo_box->setCurrentIndex(audio_out_device_index < 0 ? 0 : audio_out_device_index); + }); + audio_devices_future_watcher->setFuture(audio_devices_future); + general_layout->addRow(tr("Audio Output Device:"), audio_device_combo_box); auto about_button = new QPushButton(tr("About Chiaki"), this); general_layout->addRow(about_button); @@ -153,36 +183,6 @@ SettingsDialog::SettingsDialog(Settings *settings, QWidget *parent) : QDialog(pa audio_buffer_size_edit->setPlaceholderText(tr("Default (%1)").arg(settings->GetAudioBufferSizeDefault())); connect(audio_buffer_size_edit, &QLineEdit::textEdited, this, &SettingsDialog::AudioBufferSizeEdited); - audio_device_combo_box = new QComboBox(this); - audio_device_combo_box->addItem(tr("Auto")); - auto current_audio_device = settings->GetAudioOutDevice(); - if(!current_audio_device.isEmpty()) - { - // temporarily add the selected device before async fetching is done - audio_device_combo_box->addItem(current_audio_device, current_audio_device); - audio_device_combo_box->setCurrentIndex(1); - } - connect(audio_device_combo_box, QOverload::of(&QComboBox::activated), this, [this](){ - this->settings->SetAudioOutDevice(audio_device_combo_box->currentData().toString()); - }); - - // do this async because it's slow, assuming availableDevices() is thread-safe - auto audio_devices_future = QtConcurrent::run([]() { - return QAudioDeviceInfo::availableDevices(QAudio::AudioOutput); - }); - auto audio_devices_future_watcher = new QFutureWatcher>(this); - connect(audio_devices_future_watcher, &QFutureWatcher>::finished, this, [this, audio_devices_future_watcher, settings]() { - auto available_devices = audio_devices_future_watcher->result(); - while(audio_device_combo_box->count() > 1) // remove all but "Auto" - audio_device_combo_box->removeItem(1); - for (QAudioDeviceInfo di : available_devices) - audio_device_combo_box->addItem(di.deviceName(), di.deviceName()); - int audio_out_device_index = audio_device_combo_box->findData(settings->GetAudioOutDevice()); - audio_device_combo_box->setCurrentIndex(audio_out_device_index < 0 ? 0 : audio_out_device_index); - }); - audio_devices_future_watcher->setFuture(audio_devices_future); - general_layout->addRow(tr("Audio Output Device:"), audio_device_combo_box); - // Decode Settings auto decode_settings = new QGroupBox(tr("Decode Settings")); From dff98441323eccad435e1c68ed2a82d2a8f4e311 Mon Sep 17 00:00:00 2001 From: Tom <26638278+tomblind@users.noreply.github.com> Date: Thu, 3 Dec 2020 13:04:05 -0700 Subject: [PATCH 083/237] Connect all available controllers in GUI (#380) --- gui/include/controllermanager.h | 2 +- gui/include/streamsession.h | 4 ++-- gui/src/controllermanager.cpp | 6 +++--- gui/src/streamsession.cpp | 35 +++++++++++++++++++++------------ 4 files changed, 28 insertions(+), 19 deletions(-) diff --git a/gui/include/controllermanager.h b/gui/include/controllermanager.h index a4e6f99..b409a50 100644 --- a/gui/include/controllermanager.h +++ b/gui/include/controllermanager.h @@ -41,7 +41,7 @@ class ControllerManager : public QObject ControllerManager(QObject *parent = nullptr); ~ControllerManager(); - QList GetAvailableControllers(); + QSet GetAvailableControllers(); Controller *OpenController(int device_id); signals: diff --git a/gui/include/streamsession.h b/gui/include/streamsession.h index 01becf5..0035aef 100644 --- a/gui/include/streamsession.h +++ b/gui/include/streamsession.h @@ -68,7 +68,7 @@ class StreamSession : public QObject ChiakiOpusDecoder opus_decoder; bool connected; - Controller *controller; + QHash controllers; #if CHIAKI_GUI_ENABLE_SETSU Setsu *setsu; QMap, uint8_t> setsu_ids; @@ -111,7 +111,7 @@ class StreamSession : public QObject void SetLoginPIN(const QString &pin); - Controller *GetController() { return controller; } + QList GetControllers() { return controllers.values(); } VideoDecoder *GetVideoDecoder() { return video_decoder; } #if CHIAKI_LIB_ENABLE_PI_DECODER ChiakiPiDecoder *GetPiDecoder() { return pi_decoder; } diff --git a/gui/src/controllermanager.cpp b/gui/src/controllermanager.cpp index 4f0e5f4..cf31a5a 100644 --- a/gui/src/controllermanager.cpp +++ b/gui/src/controllermanager.cpp @@ -114,7 +114,7 @@ void ControllerManager::UpdateAvailableControllers() { if(!SDL_IsGameController(i)) continue; - + // We'll try to identify pads with Motion Control SDL_JoystickGUID guid = SDL_JoystickGetGUID(SDL_JoystickOpen(i)); char guid_str[256]; @@ -174,10 +174,10 @@ void ControllerManager::ControllerEvent(int device_id) open_controllers[device_id]->UpdateState(); } -QList ControllerManager::GetAvailableControllers() +QSet ControllerManager::GetAvailableControllers() { #ifdef CHIAKI_GUI_ENABLE_SDL_GAMECONTROLLER - return available_controllers.values(); + return available_controllers; #else return {}; #endif diff --git a/gui/src/streamsession.cpp b/gui/src/streamsession.cpp index d8f5147..23f7f80 100644 --- a/gui/src/streamsession.cpp +++ b/gui/src/streamsession.cpp @@ -43,7 +43,6 @@ static void SessionSetsuCb(SetsuEvent *event, void *user); StreamSession::StreamSession(const StreamSessionConnectInfo &connect_info, QObject *parent) : QObject(parent), log(this, connect_info.log_level_mask, connect_info.log_file), - controller(nullptr), video_decoder(nullptr), #if CHIAKI_LIB_ENABLE_PI_DECODER pi_decoder(nullptr), @@ -146,7 +145,8 @@ StreamSession::~StreamSession() chiaki_session_fini(&session); chiaki_opus_decoder_fini(&opus_decoder); #if CHIAKI_GUI_ENABLE_SDL_GAMECONTROLLER - delete controller; + for(auto controller : controllers) + delete controller; #endif #if CHIAKI_GUI_ENABLE_SETSU setsu_free(setsu); @@ -253,25 +253,31 @@ void StreamSession::HandleKeyboardEvent(QKeyEvent *event) void StreamSession::UpdateGamepads() { #if CHIAKI_GUI_ENABLE_SDL_GAMECONTROLLER - if(!controller || !controller->IsConnected()) + for(auto controller_id : controllers.keys()) { - if(controller) + auto controller = controllers[controller_id]; + if(!controller->IsConnected()) { CHIAKI_LOGI(log.GetChiakiLog(), "Controller %d disconnected", controller->GetDeviceID()); + controllers.remove(controller_id); delete controller; - controller = nullptr; } - const auto available_controllers = ControllerManager::GetInstance()->GetAvailableControllers(); - if(!available_controllers.isEmpty()) + } + + const auto available_controllers = ControllerManager::GetInstance()->GetAvailableControllers(); + for(auto controller_id : available_controllers) + { + if(!controllers.contains(controller_id)) { - controller = ControllerManager::GetInstance()->OpenController(available_controllers[0]); + auto controller = ControllerManager::GetInstance()->OpenController(controller_id); if(!controller) { - CHIAKI_LOGE(log.GetChiakiLog(), "Failed to open controller %d", available_controllers[0]); - return; + CHIAKI_LOGE(log.GetChiakiLog(), "Failed to open controller %d", controller_id); + continue; } - CHIAKI_LOGI(log.GetChiakiLog(), "Controller %d opened: \"%s\"", available_controllers[0], controller->GetName().toLocal8Bit().constData()); + CHIAKI_LOGI(log.GetChiakiLog(), "Controller %d opened: \"%s\"", controller_id, controller->GetName().toLocal8Bit().constData()); connect(controller, &Controller::StateChanged, this, &StreamSession::SendFeedbackState); + controllers[controller_id] = controller; } } @@ -285,8 +291,11 @@ void StreamSession::SendFeedbackState() chiaki_controller_state_set_idle(&state); #if CHIAKI_GUI_ENABLE_SDL_GAMECONTROLLER - if(controller) - state = controller->GetState(); + for(auto controller : controllers) + { + auto controller_state = controller->GetState(); + chiaki_controller_state_or(&state, &state, &controller_state); + } #endif #if CHIAKI_GUI_ENABLE_SETSU From 402e7f01ee1138df8b436ace15b9a30253a22faf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Tue, 22 Dec 2020 15:49:47 +0100 Subject: [PATCH 084/237] Add .cache to gitignore for clangd --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index bb63e63..3f4ed2a 100644 --- a/.gitignore +++ b/.gitignore @@ -24,4 +24,5 @@ secret.tar keystore-env.sh compile_commands.json .ccls-cache +.cache/ .gdb_history From 4da09f75f3c27e2309dd7afbf88b95b19768a736 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Wed, 23 Dec 2020 17:36:17 +0100 Subject: [PATCH 085/237] Make Borealis GUI more generic to build (#408) --- .gitignore | 1 + .gitmodules | 4 +- CMakeLists.txt | 33 +++------ cmake/FindFFMPEG.cmake | 5 ++ cmake/switch.cmake | 29 ++++---- lib/include/chiaki/stoppipe.h | 2 +- lib/src/stoppipe.c | 10 +-- scripts/switch/build.sh | 44 ++++++------ switch/CMakeLists.txt | 116 +++++++++++++++++++++++++++---- {third-party => switch}/borealis | 0 switch/res/i18n/en-US | 2 +- switch/res/inter | 2 +- switch/res/material | 2 +- switch/src/io.cpp | 2 +- switch/src/main.cpp | 12 ++-- third-party/CMakeLists.txt | 71 ------------------- 16 files changed, 168 insertions(+), 167 deletions(-) rename {third-party => switch}/borealis (100%) diff --git a/.gitignore b/.gitignore index 3f4ed2a..8cbe89f 100644 --- a/.gitignore +++ b/.gitignore @@ -26,3 +26,4 @@ compile_commands.json .ccls-cache .cache/ .gdb_history +chiaki.conf diff --git a/.gitmodules b/.gitmodules index 30d4edd..6635f42 100644 --- a/.gitmodules +++ b/.gitmodules @@ -13,6 +13,6 @@ [submodule "android/app/src/main/cpp/oboe"] path = android/app/src/main/cpp/oboe url = https://github.com/google/oboe -[submodule "third-party/borealis"] - path = third-party/borealis +[submodule "switch/borealis"] + path = switch/borealis url = https://github.com/natinusala/borealis.git diff --git a/CMakeLists.txt b/CMakeLists.txt index 4e49f8b..67303ed 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -13,7 +13,7 @@ option(CHIAKI_ENABLE_TESTS "Enable tests for Chiaki" ON) option(CHIAKI_ENABLE_CLI "Enable CLI for Chiaki" OFF) option(CHIAKI_ENABLE_GUI "Enable Qt GUI" ON) option(CHIAKI_ENABLE_ANDROID "Enable Android (Use only as part of the Gradle Project)" OFF) -option(CHIAKI_ENABLE_SWITCH "Enable Nintendo Switch (Requires devKitPro libnx)" OFF) +option(CHIAKI_ENABLE_BOREALIS "Enable Borealis GUI (For Nintendo Switch or PC)" OFF) tri_option(CHIAKI_ENABLE_SETSU "Enable libsetsu for touchpad input from controller" AUTO) option(CHIAKI_LIB_ENABLE_OPUS "Use Opus as part of Chiaki Lib" ON) tri_option(CHIAKI_ENABLE_PI_DECODER "Enable Raspberry Pi-specific video decoder (requires libraspberrypi0 and libraspberrypi-doc)" AUTO) @@ -39,33 +39,16 @@ set(CPACK_DEBIAN_PACKAGE_DESCRIPTION ${CPACK_PACKAGE_DESCRIPTION}) set(CPACK_DEBIAN_PACKAGE_SECTION "games") include(CPack) +set(CHIAKI_IS_SWITCH ${NSWITCH}) + list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake;${CMAKE_CURRENT_SOURCE_DIR}/setsu/cmake") -# configure nintendo switch toolchain -if(CHIAKI_ENABLE_SWITCH) - # load switch.cmake toolchain form ./cmake folder - # include(switch) - # to compile borealis third party - set(CMAKE_CXX_STANDARD 17) - # TODO check if android ... or other versions are enabled +set(CMAKE_CXX_STANDARD 11) + +if(CHIAKI_IS_SWITCH) # force mbedtls as crypto lib set(CHIAKI_LIB_ENABLE_MBEDTLS ON) - - if(CHIAKI_SWITCH_ENABLE_LINUX) - # CHIAKI_SWITCH_ENABLE_LINUX is a special testing version - # the aim is to troubleshoot nitendo switch chiaki verison - # from a x86 linux os - add_definitions(-DCHIAKI_SWITCH_ENABLE_LINUX) - add_definitions(-DBOREALIS_RESOURCES="./switch/res/") - set(CMAKE_BUILD_TYPE Debug) - add_definitions(-DDEBUG_OPENGL) - else() - add_definitions(-D__SWITCH__) - # use symbolic links to load font .. - add_definitions(-DBOREALIS_RESOURCES="romfs:/") - endif() -else() - set(CMAKE_CXX_STANDARD 11) + add_definitions(-D__SWITCH__) endif() if(CHIAKI_USE_SYSTEM_JERASURE) @@ -167,6 +150,6 @@ if(CHIAKI_ENABLE_ANDROID) add_subdirectory(android/app) endif() -if(CHIAKI_ENABLE_SWITCH) +if(CHIAKI_ENABLE_BOREALIS) add_subdirectory(switch) endif() diff --git a/cmake/FindFFMPEG.cmake b/cmake/FindFFMPEG.cmake index a1d0ecd..32e8ac0 100644 --- a/cmake/FindFFMPEG.cmake +++ b/cmake/FindFFMPEG.cmake @@ -42,6 +42,11 @@ function(join OUTPUT GLUE) endfunction() function (_ffmpeg_find component headername) + if(TARGET "FFMPEG::${component}") + # already found before + return() + endif() + # Try pkg-config first if(PKG_CONFIG_FOUND) pkg_check_modules(FFMPEG_${component} lib${component} IMPORTED_TARGET) diff --git a/cmake/switch.cmake b/cmake/switch.cmake index 05c18b9..6046d67 100644 --- a/cmake/switch.cmake +++ b/cmake/switch.cmake @@ -1,14 +1,16 @@ + # Find DEVKITPRO -if(NOT DEFINED ENV{DEVKITPRO} OR NOT DEFINED ENV{PORTLIBS_PREFIX}) +set(DEVKITPRO "$ENV{DEVKITPRO}" CACHE PATH "Path to DevKitPro") +set(PORTLIBS_PREFIX "$ENV{PORTLIBS_PREFIX}" CACHE PATH "Path to portlibs inside DevKitPro") +if(NOT DEVKITPRO OR NOT PORTLIBS_PREFIX) message(FATAL_ERROR "Please set DEVKITPRO & PORTLIBS_PREFIX env before calling cmake. https://devkitpro.org/wiki/Getting_Started") endif() -set(DEVKITPRO "$ENV{DEVKITPRO}") -set(PORTLIBS_PREFIX "$ENV{PORTLIBS_PREFIX}") - # include devkitpro toolchain include("${DEVKITPRO}/switch.cmake") +set(NSWITCH TRUE) + # Enable gcc -g, to use # /opt/devkitpro/devkitA64/bin/aarch64-none-elf-addr2line -e build_switch/switch/chiaki -f -p -C -a 0xCCB5C # set(CMAKE_BUILD_TYPE Debug) @@ -60,20 +62,21 @@ function(__add_nacp target APP_TITLE APP_AUTHOR APP_VERSION) ) endfunction() -function(add_nro_target target title author version icon romfs) - get_filename_component(target_we ${target} NAME_WE) - if(NOT ${target_we}.nacp) - __add_nacp(${target_we}.nacp ${title} ${author} ${version}) +function(add_nro_target output_name target title author version icon romfs) + if(NOT ${output_name}.nacp) + __add_nacp(${output_name}.nacp ${title} ${author} ${version}) endif() - add_custom_command(OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/${target_we}.nro + add_custom_command(OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/${output_name}.nro COMMAND ${ELF2NRO} $ - ${CMAKE_CURRENT_BINARY_DIR}/${target_we}.nro + ${CMAKE_CURRENT_BINARY_DIR}/${output_name}.nro --icon=${icon} - --nacp=${CMAKE_CURRENT_BINARY_DIR}/${target_we}.nacp + --nacp=${CMAKE_CURRENT_BINARY_DIR}/${output_name}.nacp --romfsdir=${romfs} - DEPENDS ${target} ${CMAKE_CURRENT_BINARY_DIR}/${target_we}.nacp + DEPENDS ${target} ${CMAKE_CURRENT_BINARY_DIR}/${output_name}.nacp VERBATIM ) - add_custom_target(${target_we}_nro ALL SOURCES ${CMAKE_CURRENT_BINARY_DIR}/${target_we}.nro) + add_custom_target(${output_name}_nro ALL SOURCES ${CMAKE_CURRENT_BINARY_DIR}/${output_name}.nro) endfunction() +set(CMAKE_USE_SYSTEM_ENVIRONMENT_PATH OFF) +set(CMAKE_PREFIX_PATH "/") diff --git a/lib/include/chiaki/stoppipe.h b/lib/include/chiaki/stoppipe.h index 968da8b..60715d1 100644 --- a/lib/include/chiaki/stoppipe.h +++ b/lib/include/chiaki/stoppipe.h @@ -23,7 +23,7 @@ typedef struct chiaki_stop_pipe_t { #ifdef _WIN32 WSAEVENT event; -#elif defined(__SWITCH__) || defined(CHIAKI_ENABLE_SWITCH_LINUX) +#elif defined(__SWITCH__) // due to a lack pipe/event/socketpair // on switch env, we use a physical socket // to send/trigger the cancel signal diff --git a/lib/src/stoppipe.c b/lib/src/stoppipe.c index 57020c2..9b699d9 100644 --- a/lib/src/stoppipe.c +++ b/lib/src/stoppipe.c @@ -19,7 +19,7 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_stop_pipe_init(ChiakiStopPipe *stop_pipe) stop_pipe->event = WSACreateEvent(); if(stop_pipe->event == WSA_INVALID_EVENT) return CHIAKI_ERR_UNKNOWN; -#elif defined(__SWITCH__) || defined(CHIAKI_ENABLE_SWITCH_LINUX) +#elif defined(__SWITCH__) // currently pipe or socketpare are not available on switch // use a custom udp socket as pipe @@ -64,7 +64,7 @@ CHIAKI_EXPORT void chiaki_stop_pipe_fini(ChiakiStopPipe *stop_pipe) { #ifdef _WIN32 WSACloseEvent(stop_pipe->event); -#elif defined(__SWITCH__) || defined(CHIAKI_ENABLE_SWITCH_LINUX) +#elif defined(__SWITCH__) close(stop_pipe->fd); #else close(stop_pipe->fds[0]); @@ -76,7 +76,7 @@ CHIAKI_EXPORT void chiaki_stop_pipe_stop(ChiakiStopPipe *stop_pipe) { #ifdef _WIN32 WSASetEvent(stop_pipe->event); -#elif defined(__SWITCH__) || defined(CHIAKI_ENABLE_SWITCH_LINUX) +#elif defined(__SWITCH__) // send to local socket (FIXME MSG_CONFIRM) sendto(stop_pipe->fd, "\x00", 1, 0, (struct sockaddr*)&stop_pipe->addr, sizeof(struct sockaddr_in)); @@ -120,7 +120,7 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_stop_pipe_select_single(ChiakiStopPipe *sto #else fd_set rfds; FD_ZERO(&rfds); -#if defined(__SWITCH__) || defined(CHIAKI_ENABLE_SWITCH_LINUX) +#if defined(__SWITCH__) // push udp local socket as fd int stop_fd = stop_pipe->fd; #else @@ -244,7 +244,7 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_stop_pipe_reset(ChiakiStopPipe *stop_pipe) #ifdef _WIN32 BOOL r = WSAResetEvent(stop_pipe->event); return r ? CHIAKI_ERR_SUCCESS : CHIAKI_ERR_UNKNOWN; -#elif defined(__SWITCH__) || defined(CHIAKI_ENABLE_SWITCH_LINUX) +#elif defined(__SWITCH__) //FIXME uint8_t v; int r; diff --git a/scripts/switch/build.sh b/scripts/switch/build.sh index bbcb729..1e03a2a 100755 --- a/scripts/switch/build.sh +++ b/scripts/switch/build.sh @@ -3,40 +3,36 @@ set -xveo pipefail arg1=$1 -CHIAKI_SWITCH_ENABLE_LINUX="ON" build="./build" if [ "$arg1" != "linux" ]; then - CHIAKI_SWITCH_ENABLE_LINUX="OFF" - # source /opt/devkitpro/switchvars.sh - # toolchain="${DEVKITPRO}/switch.cmake" - toolchain="cmake/switch.cmake" - export PORTLIBS_PREFIX="$(${DEVKITPRO}/portlibs_prefix.sh switch)" - build="./build_switch" + # source /opt/devkitpro/switchvars.sh + # toolchain="${DEVKITPRO}/switch.cmake" + toolchain="cmake/switch.cmake" + export PORTLIBS_PREFIX="$(${DEVKITPRO}/portlibs_prefix.sh switch)" + build="./build_switch" fi SCRIPTDIR=$(dirname "$0") BASEDIR=$(realpath "${SCRIPTDIR}/../../") build_chiaki (){ - pushd "${BASEDIR}" - #rm -rf ./build + pushd "${BASEDIR}" + #rm -rf ./build - cmake -B "${build}" \ - -DCMAKE_TOOLCHAIN_FILE=${toolchain} \ - -DCHIAKI_ENABLE_TESTS=OFF \ - -DCHIAKI_ENABLE_CLI=OFF \ - -DCHIAKI_ENABLE_GUI=OFF \ - -DCHIAKI_ENABLE_ANDROID=OFF \ - -DCHIAKI_ENABLE_SWITCH=ON \ - -DCHIAKI_SWITCH_ENABLE_LINUX="${CHIAKI_SWITCH_ENABLE_LINUX}" \ - -DCHIAKI_LIB_ENABLE_MBEDTLS=ON \ - # -DCMAKE_VERBOSE_MAKEFILE:BOOL=ON \ - # -DCMAKE_FIND_DEBUG_MODE=ON + cmake -B "${build}" \ + -GNinja \ + -DCMAKE_TOOLCHAIN_FILE=${toolchain} \ + -DCHIAKI_ENABLE_TESTS=OFF \ + -DCHIAKI_ENABLE_CLI=OFF \ + -DCHIAKI_ENABLE_GUI=OFF \ + -DCHIAKI_ENABLE_ANDROID=OFF \ + -DCHIAKI_ENABLE_BOREALIS=ON \ + -DCHIAKI_LIB_ENABLE_MBEDTLS=ON \ + # -DCMAKE_VERBOSE_MAKEFILE:BOOL=ON \ + # -DCMAKE_FIND_DEBUG_MODE=ON - pushd "${BASEDIR}/${build}" - make -j8 - popd - popd + ninja -C "${build}" + popd } build_chiaki diff --git a/switch/CMakeLists.txt b/switch/CMakeLists.txt index 62fa146..89bd789 100644 --- a/switch/CMakeLists.txt +++ b/switch/CMakeLists.txt @@ -1,12 +1,93 @@ -set(CMAKE_INCLUDE_CURRENT_DIR ON) +################## +# borealis dependency +################## + +# do not include +# borealis/library/lib/switch_wrapper.c +# switch functions are in switch/src/main.cpp +set(BOREALIS_SOURCE + borealis/library/lib/extern/libretro-common/features/features_cpu.c + borealis/library/lib/extern/libretro-common/encodings/encoding_utf.c + borealis/library/lib/extern/libretro-common/compat/compat_strl.c + borealis/library/lib/extern/nxfmtwrapper/format.cpp + borealis/library/lib/extern/nanovg/nanovg.c + borealis/library/lib/extern/glad/glad.c + borealis/library/lib/scroll_view.cpp + borealis/library/lib/style.cpp + borealis/library/lib/table.cpp + borealis/library/lib/task_manager.cpp + borealis/library/lib/progress_display.cpp + borealis/library/lib/staged_applet_frame.cpp + borealis/library/lib/applet_frame.cpp + borealis/library/lib/hint.cpp + borealis/library/lib/image.cpp + borealis/library/lib/logger.cpp + borealis/library/lib/swkbd.cpp + borealis/library/lib/crash_frame.cpp + borealis/library/lib/header.cpp + borealis/library/lib/progress_spinner.cpp + borealis/library/lib/layer_view.cpp + borealis/library/lib/notification_manager.cpp + borealis/library/lib/rectangle.cpp + borealis/library/lib/application.cpp + borealis/library/lib/box_layout.cpp + borealis/library/lib/sidebar.cpp + borealis/library/lib/dropdown.cpp + borealis/library/lib/popup_frame.cpp + borealis/library/lib/repeating_task.cpp + borealis/library/lib/absolute_layout.cpp + borealis/library/lib/i18n.cpp + borealis/library/lib/tab_frame.cpp + borealis/library/lib/thumbnail_frame.cpp + borealis/library/lib/animations.cpp + borealis/library/lib/dialog.cpp + borealis/library/lib/view.cpp + borealis/library/lib/list.cpp + borealis/library/lib/button.cpp + borealis/library/lib/label.cpp + borealis/library/lib/theme.cpp + borealis/library/lib/material_icon.cpp) + +add_library(borealis STATIC ${BOREALIS_SOURCE}) +set_property(TARGET borealis PROPERTY CXX_STANDARD 17) +target_include_directories(borealis PUBLIC + borealis/library/include + borealis/library/include/borealis/extern + borealis/library/include/borealis/extern/glad + borealis/library/include/borealis/extern/nanovg + borealis/library/include/borealis/extern/libretro-common + borealis/library/lib/extern/fmt/include) + +find_package(glfw3 REQUIRED) +find_library(EGL EGL) +find_library(GLAPI glapi) +find_library(DRM_NOUVEAU drm_nouveau) +target_link_libraries(borealis + glfw + ${EGL} + ${GLAPI} + ${DRM_NOUVEAU}) + +if(CHIAKI_IS_SWITCH) + target_compile_definitions(borealis PUBLIC + BOREALIS_RESOURCES="romfs:/") +else() + target_compile_definitions(borealis PUBLIC + BOREALIS_RESOURCES="./switch/res/") +endif() + + +################## +# chiaki with borealis +################## find_package(FFMPEG REQUIRED COMPONENTS avcodec avutil swscale) find_library(SDL2 SDL2) find_library(SWRESAMPLE swresample) # find -type f | grep -P '\.(h|cpp)$' | sed 's#\./#\t\t#g' -add_executable(chiaki WIN32 +add_executable(chiaki-borealis WIN32 src/discoverymanager.cpp src/settings.cpp src/io.cpp @@ -14,9 +95,13 @@ add_executable(chiaki WIN32 src/main.cpp src/gui.cpp) -target_include_directories(chiaki PRIVATE include) +set_target_properties(chiaki-borealis PROPERTIES + CXX_STANDARD 17 + OUTPUT_NAME chiaki) -target_link_libraries(chiaki +target_include_directories(chiaki-borealis PRIVATE include) + +target_link_libraries(chiaki-borealis chiaki-lib borealis ${SDL2} @@ -26,21 +111,24 @@ target_link_libraries(chiaki ${SWRESAMPLE} ${SWSCALE}) -# OS links -if(CHIAKI_SWITCH_ENABLE_LINUX) - target_link_libraries(chiaki dl) -else() +if(CHIAKI_IS_SWITCH) # libnx is forced by the switch toolchain find_library(Z z) - find_library(GLAPI glapi) - find_library(DRM_NOUVEAU drm_nouveau) - target_link_libraries(chiaki ${Z} ${GLAPI} ${DRM_NOUVEAU}) + find_library(GLAPI glapi) # TODO: make it transitive from borealis + find_library(DRM_NOUVEAU drm_nouveau) # TODO: make it transitive from borealis + target_link_libraries(chiaki-borealis ${Z} ${GLAPI} ${DRM_NOUVEAU}) endif() -install(TARGETS chiaki +install(TARGETS chiaki-borealis RUNTIME DESTINATION bin BUNDLE DESTINATION bin) -if(CHIAKI_ENABLE_SWITCH AND NOT CHIAKI_SWITCH_ENABLE_LINUX) - add_nro_target(chiaki "chiaki" "Chiaki team" "${CHIAKI_VERSION}" "${CMAKE_SOURCE_DIR}/switch/res/icon.jpg" "${CMAKE_SOURCE_DIR}/switch/res") +if(CHIAKI_IS_SWITCH) + add_nro_target(chiaki + chiaki-borealis + "chiaki" + "Chiaki team" + "${CHIAKI_VERSION}" + "${CMAKE_SOURCE_DIR}/switch/res/icon.jpg" + "${CMAKE_SOURCE_DIR}/switch/res") endif() diff --git a/third-party/borealis b/switch/borealis similarity index 100% rename from third-party/borealis rename to switch/borealis diff --git a/switch/res/i18n/en-US b/switch/res/i18n/en-US index dec3de9..8a42ec7 120000 --- a/switch/res/i18n/en-US +++ b/switch/res/i18n/en-US @@ -1 +1 @@ -../../../third-party/borealis/resources/i18n/en-US \ No newline at end of file +../../borealis/resources/i18n/en-US \ No newline at end of file diff --git a/switch/res/inter b/switch/res/inter index 7a6e86a..43ad663 120000 --- a/switch/res/inter +++ b/switch/res/inter @@ -1 +1 @@ -../../third-party/borealis/resources/inter \ No newline at end of file +../borealis/resources/inter \ No newline at end of file diff --git a/switch/res/material b/switch/res/material index 8200753..30bcf28 120000 --- a/switch/res/material +++ b/switch/res/material @@ -1 +1 @@ -../../third-party/borealis/resources/material \ No newline at end of file +../borealis/resources/material \ No newline at end of file diff --git a/switch/src/io.cpp b/switch/src/io.cpp index 476f8a5..3b02f83 100644 --- a/switch/src/io.cpp +++ b/switch/src/io.cpp @@ -318,7 +318,7 @@ bool IO::FreeVideo() bool IO::ReadUserKeyboard(char *buffer, size_t buffer_size) { -#ifdef CHIAKI_SWITCH_ENABLE_LINUX +#ifndef __SWITCH__ // use cin to get user input from linux std::cin.getline(buffer, buffer_size); CHIAKI_LOGI(this->log, "Got user input: %s\n", buffer); diff --git a/switch/src/main.cpp b/switch/src/main.cpp index 1ff85a0..1a48318 100644 --- a/switch/src/main.cpp +++ b/switch/src/main.cpp @@ -20,7 +20,7 @@ bool appletMainLoop() } #endif -#ifndef CHIAKI_SWITCH_ENABLE_LINUX +#if __SWITCH__ #define CHIAKI_ENABLE_SWITCH_NXLINK 1 #endif @@ -109,15 +109,11 @@ int main(int argc, char* argv[]) { // init chiaki lib ChiakiLog log; -#if defined(CHIAKI_ENABLE_SWITCH_NXLINK) || defined(CHIAKI_SWITCH_ENABLE_LINUX) -#ifdef __SWITCH__ - chiaki_log_init(&log, CHIAKI_LOG_ALL ^ CHIAKI_LOG_VERBOSE, chiaki_log_cb_print, NULL); -#else - chiaki_log_init(&log, CHIAKI_LOG_ALL, chiaki_log_cb_print, NULL); -#endif -#else +#if defined(__SWITCH__) && !defined(CHIAKI_ENABLE_SWITCH_NXLINK) // null log for switch version chiaki_log_init(&log, 0, chiaki_log_cb_print, NULL); +#else + chiaki_log_init(&log, CHIAKI_LOG_ALL ^ CHIAKI_LOG_VERBOSE, chiaki_log_cb_print, NULL); #endif // load chiaki lib diff --git a/third-party/CMakeLists.txt b/third-party/CMakeLists.txt index 33fbfbc..b1f0b46 100644 --- a/third-party/CMakeLists.txt +++ b/third-party/CMakeLists.txt @@ -52,74 +52,3 @@ if(NOT CHIAKI_USE_SYSTEM_JERASURE) add_library(Jerasure::Jerasure ALIAS jerasure) endif() - -################## -# borealis -################## - -if(CHIAKI_ENABLE_SWITCH) - # do not include - # borealis/library/lib/switch_wrapper.c - # switch functions are in switch/src/main.cpp - set(BOREALIS_SOURCE - borealis/library/lib/extern/libretro-common/features/features_cpu.c - borealis/library/lib/extern/libretro-common/encodings/encoding_utf.c - borealis/library/lib/extern/libretro-common/compat/compat_strl.c - borealis/library/lib/extern/nxfmtwrapper/format.cpp - borealis/library/lib/extern/nanovg/nanovg.c - borealis/library/lib/extern/glad/glad.c - borealis/library/lib/scroll_view.cpp - borealis/library/lib/style.cpp - borealis/library/lib/table.cpp - borealis/library/lib/task_manager.cpp - borealis/library/lib/progress_display.cpp - borealis/library/lib/staged_applet_frame.cpp - borealis/library/lib/applet_frame.cpp - borealis/library/lib/hint.cpp - borealis/library/lib/image.cpp - borealis/library/lib/logger.cpp - borealis/library/lib/swkbd.cpp - borealis/library/lib/crash_frame.cpp - borealis/library/lib/header.cpp - borealis/library/lib/progress_spinner.cpp - borealis/library/lib/layer_view.cpp - borealis/library/lib/notification_manager.cpp - borealis/library/lib/rectangle.cpp - borealis/library/lib/application.cpp - borealis/library/lib/box_layout.cpp - borealis/library/lib/sidebar.cpp - borealis/library/lib/dropdown.cpp - borealis/library/lib/popup_frame.cpp - borealis/library/lib/repeating_task.cpp - borealis/library/lib/absolute_layout.cpp - borealis/library/lib/i18n.cpp - borealis/library/lib/tab_frame.cpp - borealis/library/lib/thumbnail_frame.cpp - borealis/library/lib/animations.cpp - borealis/library/lib/dialog.cpp - borealis/library/lib/view.cpp - borealis/library/lib/list.cpp - borealis/library/lib/button.cpp - borealis/library/lib/label.cpp - borealis/library/lib/theme.cpp - borealis/library/lib/material_icon.cpp) - - add_library(borealis STATIC ${BOREALIS_SOURCE}) - target_include_directories(borealis PUBLIC - borealis/library/include - borealis/library/include/borealis/extern - borealis/library/include/borealis/extern/glad - borealis/library/include/borealis/extern/nanovg - borealis/library/include/borealis/extern/libretro-common - borealis/library/lib/extern/fmt/include) - - find_package(glfw3 REQUIRED) - find_library(EGL EGL) - find_library(GLAPI glapi) - find_library(DRM_NOUVEAU drm_nouveau) - target_link_libraries(borealis - glfw - ${EGL} - ${GLAPI} - ${DRM_NOUVEAU}) -endif() From 4d3f2aadbf3bd32372d715e3911a8945dbc0d705 Mon Sep 17 00:00:00 2001 From: H0neyBadger Date: Wed, 23 Dec 2020 18:43:22 +0100 Subject: [PATCH 086/237] Improve Switch Registration Logs and Audio Level (#409) --- switch/src/host.cpp | 11 +++++++++-- switch/src/io.cpp | 21 +++++++++++++++++++-- 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/switch/src/host.cpp b/switch/src/host.cpp index 200f74d..2315975 100644 --- a/switch/src/host.cpp +++ b/switch/src/host.cpp @@ -118,13 +118,20 @@ int Host::Register(std::string pin) else { CHIAKI_LOGE(this->log, "Undefined PS4 system version (please run discover first)"); + throw Exception("Undefined PS4 system version (please run discover first)"); } this->regist_info.pin = atoi(pin.c_str()); this->regist_info.host = this->host_addr.c_str(); this->regist_info.broadcast = false; - CHIAKI_LOGI(this->log, "Registering to host `%s` `%s` with PSN AccountID `%s` pin `%s`", - this->host_name.c_str(), this->host_addr.c_str(), psn_account_id.c_str(), pin.c_str()); + + if(this->system_version >= 7000000) + CHIAKI_LOGI(this->log, "Registering to host `%s` `%s` with PSN AccountID `%s` pin `%s`", + this->host_name.c_str(), this->host_addr.c_str(), account_id.c_str(), pin.c_str()); + else + CHIAKI_LOGI(this->log, "Registering to host `%s` `%s` with PSN OnlineID `%s` pin `%s`", + this->host_name.c_str(), this->host_addr.c_str(), online_id.c_str(), pin.c_str()); + chiaki_regist_start(&this->regist, this->log, &this->regist_info, RegistEventCB, this); return HOST_REGISTER_OK; } diff --git a/switch/src/io.cpp b/switch/src/io.cpp index 3b02f83..3e15bd0 100644 --- a/switch/src/io.cpp +++ b/switch/src/io.cpp @@ -243,8 +243,25 @@ void IO::InitAudioCB(unsigned int channels, unsigned int rate) void IO::AudioCB(int16_t * buf, size_t samples_count) { - //int az = SDL_GetQueuedAudioSize(host->audio_device_id); - // len the number of bytes (not samples!) to which (data) points + for(int x=0; x < samples_count*2; x++) + { + // boost audio volume + int sample = buf[x]*1.80; + // Hard clipping (audio compression) + // truncate value that overflow/underflow int16 + if(sample > INT16_MAX) + { + buf[x] = INT16_MAX; + CHIAKI_LOGD(this->log, "Audio Hard clipping INT16_MAX < %d", sample); + } + else if(sample < INT16_MIN) + { + buf[x] = INT16_MIN; + CHIAKI_LOGD(this->log, "Audio Hard clipping INT16_MIN > %d", sample); + } + else + buf[x] = (int16_t) sample; + } int success = SDL_QueueAudio(this->sdl_audio_device_id, buf, sizeof(int16_t)*samples_count*2); if(success != 0) CHIAKI_LOGE(this->log, "SDL_QueueAudio failed: %s\n", SDL_GetError()); From bf929cacd029bfbe7005298716deab05a9012f5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Sun, 27 Dec 2020 16:46:09 +0100 Subject: [PATCH 087/237] Relicense under AGPL v3 only + OpenSSL (#412) --- COPYING | 143 ++++++++---------- ...-OpenSSL.txt => AGPL-3.0-only-OpenSSL.txt} | 143 ++++++++---------- README.md | 14 +- android/app/src/main/cpp/audio-decoder.c | 2 +- android/app/src/main/cpp/audio-decoder.h | 2 +- android/app/src/main/cpp/audio-output.cpp | 2 +- android/app/src/main/cpp/audio-output.h | 2 +- android/app/src/main/cpp/chiaki-jni.c | 2 +- android/app/src/main/cpp/chiaki-jni.h | 2 +- android/app/src/main/cpp/circular-buf.hpp | 2 +- android/app/src/main/cpp/log.c | 2 +- android/app/src/main/cpp/log.h | 2 +- android/app/src/main/cpp/video-decoder.c | 2 +- android/app/src/main/cpp/video-decoder.h | 2 +- .../com/metallic/chiaki/common/AppDatabase.kt | 2 +- .../com/metallic/chiaki/common/DisplayHost.kt | 2 +- .../com/metallic/chiaki/common/LogManager.kt | 2 +- .../com/metallic/chiaki/common/MacAddress.kt | 2 +- .../com/metallic/chiaki/common/ManualHost.kt | 2 +- .../com/metallic/chiaki/common/Preferences.kt | 2 +- .../metallic/chiaki/common/RegisteredHost.kt | 2 +- .../chiaki/common/SerializedSettings.kt | 2 +- .../chiaki/common/ext/RevealActivity.kt | 2 +- .../metallic/chiaki/common/ext/RxLiveData.kt | 2 +- .../metallic/chiaki/common/ext/StringHex.kt | 2 +- .../chiaki/common/ext/ViewGroupInflate.kt | 2 +- .../chiaki/discovery/DiscoveryManager.kt | 2 +- .../main/DisplayHostRecyclerViewAdapter.kt | 2 +- .../FloatingActionButtonBackgroundBehavior.kt | 2 +- .../FloatingActionButtonSpeedDialBehavior.kt | 2 +- .../com/metallic/chiaki/main/MainActivity.kt | 2 +- .../com/metallic/chiaki/main/MainViewModel.kt | 2 +- .../EditManualConsoleActivity.kt | 2 +- .../EditManualConsoleViewModel.kt | 2 +- .../com/metallic/chiaki/regist/ChiakiRxLog.kt | 2 +- .../metallic/chiaki/regist/RegistActivity.kt | 2 +- .../chiaki/regist/RegistExecuteActivity.kt | 2 +- .../chiaki/regist/RegistExecuteViewModel.kt | 2 +- .../metallic/chiaki/regist/RegistViewModel.kt | 2 +- .../metallic/chiaki/session/StreamSession.kt | 2 +- .../chiaki/settings/ItemTouchSwipeCallback.kt | 2 +- .../chiaki/settings/SettingsActivity.kt | 2 +- .../chiaki/settings/SettingsFragment.kt | 2 +- .../chiaki/settings/SettingsLogsAdapter.kt | 2 +- .../chiaki/settings/SettingsLogsFragment.kt | 2 +- .../chiaki/settings/SettingsLogsViewModel.kt | 2 +- .../SettingsRegisteredHostsAdapter.kt | 2 +- .../SettingsRegisteredHostsFragment.kt | 2 +- .../SettingsRegisteredHostsViewModel.kt | 2 +- .../chiaki/settings/SettingsViewModel.kt | 2 +- .../metallic/chiaki/stream/StreamActivity.kt | 2 +- .../metallic/chiaki/stream/StreamViewModel.kt | 2 +- .../chiaki/touchcontrols/AnalogStickView.kt | 2 +- .../chiaki/touchcontrols/ButtonView.kt | 2 +- .../touchcontrols/ControlsBackgroundView.kt | 2 +- .../metallic/chiaki/touchcontrols/DPadView.kt | 2 +- .../touchcontrols/TouchControlsFragment.kt | 2 +- .../chiaki/touchcontrols/TouchTracker.kt | 2 +- .../touchcontrols/TouchpadOnlyFragment.kt | 2 +- .../metallic/chiaki/touchcontrols/Vector.kt | 2 +- cli/include/chiaki-cli.h | 2 +- cli/src/discover.c | 2 +- cli/src/main.c | 17 +-- cli/src/wakeup.c | 2 +- gui/CMakeLists.txt | 2 +- gui/include/avopenglframeuploader.h | 2 +- gui/include/avopenglwidget.h | 2 +- gui/include/controllermanager.h | 2 +- gui/include/discoverymanager.h | 2 +- gui/include/dynamicgridwidget.h | 2 +- gui/include/exception.h | 2 +- gui/include/host.h | 2 +- gui/include/loginpindialog.h | 2 +- gui/include/mainwindow.h | 2 +- gui/include/manualhostdialog.h | 2 +- gui/include/registdialog.h | 2 +- gui/include/servericonwidget.h | 2 +- gui/include/serveritemwidget.h | 2 +- gui/include/sessionlog.h | 2 +- gui/include/settings.h | 2 +- gui/include/settingsdialog.h | 2 +- gui/include/settingskeycapturedialog.h | 2 +- gui/include/streamsession.h | 2 +- gui/include/streamwindow.h | 2 +- gui/include/videodecoder.h | 2 +- gui/re.chiaki.Chiaki.appdata.xml | 2 +- gui/src/avopenglframeuploader.cpp | 2 +- gui/src/avopenglwidget.cpp | 2 +- gui/src/controllermanager.cpp | 2 +- gui/src/discoverymanager.cpp | 2 +- gui/src/dynamicgridwidget.cpp | 2 +- gui/src/host.cpp | 2 +- gui/src/loginpindialog.cpp | 2 +- gui/src/mainwindow.cpp | 2 +- gui/src/manualhostdialog.cpp | 2 +- gui/src/registdialog.cpp | 2 +- gui/src/servericonwidget.cpp | 2 +- gui/src/serveritemwidget.cpp | 2 +- gui/src/sessionlog.cpp | 2 +- gui/src/settings.cpp | 2 +- gui/src/settingsdialog.cpp | 7 +- gui/src/settingskeycapturedialog.cpp | 2 +- gui/src/streamsession.cpp | 2 +- gui/src/streamwindow.cpp | 2 +- gui/src/videodecoder.cpp | 2 +- lib/config.h.in | 2 +- lib/include/chiaki/audio.h | 2 +- lib/include/chiaki/audioreceiver.h | 2 +- lib/include/chiaki/base64.h | 2 +- lib/include/chiaki/common.h | 2 +- lib/include/chiaki/congestioncontrol.h | 2 +- lib/include/chiaki/controller.h | 2 +- lib/include/chiaki/ctrl.h | 2 +- lib/include/chiaki/discovery.h | 2 +- lib/include/chiaki/discoveryservice.h | 2 +- lib/include/chiaki/ecdh.h | 2 +- lib/include/chiaki/fec.h | 2 +- lib/include/chiaki/feedback.h | 2 +- lib/include/chiaki/feedbacksender.h | 2 +- lib/include/chiaki/frameprocessor.h | 2 +- lib/include/chiaki/gkcrypt.h | 2 +- lib/include/chiaki/http.h | 2 +- lib/include/chiaki/launchspec.h | 2 +- lib/include/chiaki/log.h | 2 +- lib/include/chiaki/opusdecoder.h | 2 +- lib/include/chiaki/packetstats.h | 2 +- lib/include/chiaki/random.h | 2 +- lib/include/chiaki/regist.h | 2 +- lib/include/chiaki/reorderqueue.h | 2 +- lib/include/chiaki/rpcrypt.h | 2 +- lib/include/chiaki/senkusha.h | 2 +- lib/include/chiaki/seqnum.h | 2 +- lib/include/chiaki/session.h | 2 +- lib/include/chiaki/sock.h | 2 +- lib/include/chiaki/stoppipe.h | 2 +- lib/include/chiaki/streamconnection.h | 2 +- lib/include/chiaki/takion.h | 2 +- lib/include/chiaki/takionsendbuffer.h | 2 +- lib/include/chiaki/thread.h | 2 +- lib/include/chiaki/time.h | 2 +- lib/include/chiaki/video.h | 2 +- lib/include/chiaki/videoreceiver.h | 2 +- lib/src/audio.c | 2 +- lib/src/audioreceiver.c | 2 +- lib/src/base64.c | 2 +- lib/src/common.c | 2 +- lib/src/congestioncontrol.c | 2 +- lib/src/controller.c | 2 +- lib/src/ctrl.c | 2 +- lib/src/discovery.c | 2 +- lib/src/discoveryservice.c | 2 +- lib/src/ecdh.c | 2 +- lib/src/fec.c | 2 +- lib/src/feedback.c | 2 +- lib/src/feedbacksender.c | 2 +- lib/src/frameprocessor.c | 2 +- lib/src/gkcrypt.c | 2 +- lib/src/http.c | 2 +- lib/src/launchspec.c | 2 +- lib/src/log.c | 2 +- lib/src/opusdecoder.c | 2 +- lib/src/packetstats.c | 2 +- lib/src/pb_utils.h | 2 +- lib/src/random.c | 2 +- lib/src/regist.c | 2 +- lib/src/reorderqueue.c | 2 +- lib/src/rpcrypt.c | 2 +- lib/src/senkusha.c | 2 +- lib/src/session.c | 2 +- lib/src/sock.c | 2 +- lib/src/stoppipe.c | 2 +- lib/src/streamconnection.c | 2 +- lib/src/takion.c | 2 +- lib/src/takionsendbuffer.c | 2 +- lib/src/thread.c | 2 +- lib/src/time.c | 2 +- lib/src/utils.h | 2 +- lib/src/videoreceiver.c | 2 +- setsu/demo/main.c | 2 +- setsu/include/setsu.h | 2 +- setsu/src/setsu.c | 2 +- switch/include/discoverymanager.h | 2 +- switch/include/exception.h | 2 +- switch/include/gui.h | 2 +- switch/include/host.h | 2 +- switch/include/io.h | 2 +- switch/include/settings.h | 2 +- switch/src/discoverymanager.cpp | 2 +- switch/src/gui.cpp | 2 +- switch/src/host.cpp | 2 +- switch/src/io.cpp | 2 +- switch/src/main.cpp | 2 +- switch/src/settings.cpp | 2 +- test/fec.c | 2 +- test/gkcrypt.c | 2 +- test/http.c | 2 +- test/keystate.c | 2 +- test/main.c | 2 +- test/regist.c | 2 +- test/reorderqueue.c | 2 +- test/rpcrypt.c | 2 +- test/seqnum.c | 2 +- test/takion.c | 2 +- test/test_log.c | 2 +- test/test_log.h | 2 +- 205 files changed, 340 insertions(+), 384 deletions(-) rename LICENSES/{GPL-3.0-or-later-OpenSSL.txt => AGPL-3.0-only-OpenSSL.txt} (86%) diff --git a/COPYING b/COPYING index dbc3698..c3ef0cb 100644 --- a/COPYING +++ b/COPYING @@ -1,5 +1,5 @@ - GNU GENERAL PUBLIC LICENSE - Version 3, 29 June 2007 + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies @@ -7,17 +7,15 @@ Preamble - The GNU General Public License is a free, copyleft license for -software and other kinds of works. + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, -the GNU General Public License is intended to guarantee your freedom to +our General Public Licenses are intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free -software for all its users. We, the Free Software Foundation, use the -GNU General Public License for most of our software; it applies also to -any other work released this way by its authors. You can apply it to -your programs, too. +software for all its users. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you @@ -26,44 +24,34 @@ them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. - To protect your rights, we need to prevent others from denying you -these rights or asking you to surrender the rights. Therefore, you have -certain responsibilities if you distribute copies of the software, or if -you modify it: responsibilities to respect the freedom of others. + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. - For example, if you distribute copies of such a program, whether -gratis or for a fee, you must pass on to the recipients the same -freedoms that you received. You must make sure that they, too, receive -or can get the source code. And you must show them these terms so they -know their rights. + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. - Developers that use the GNU GPL protect your rights with two steps: -(1) assert copyright on the software, and (2) offer you this License -giving you legal permission to copy, distribute and/or modify it. + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. - For the developers' and authors' protection, the GPL clearly explains -that there is no warranty for this free software. For both users' and -authors' sake, the GPL requires that modified versions be marked as -changed, so that their problems will not be attributed erroneously to -authors of previous versions. - - Some devices are designed to deny users access to install or run -modified versions of the software inside them, although the manufacturer -can do so. This is fundamentally incompatible with the aim of -protecting users' freedom to change the software. The systematic -pattern of such abuse occurs in the area of products for individuals to -use, which is precisely where it is most unacceptable. Therefore, we -have designed this version of the GPL to prohibit the practice for those -products. If such problems arise substantially in other domains, we -stand ready to extend this provision to those domains in future versions -of the GPL, as needed to protect the freedom of users. - - Finally, every program is threatened constantly by software patents. -States should not allow patents to restrict development and use of -software on general-purpose computers, but in those that do, we wish to -avoid the special danger that patents applied to a free program could -make it effectively proprietary. To prevent this, the GPL assures that -patents cannot be used to render the program non-free. + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. The precise terms and conditions for copying, distribution and modification follow. @@ -72,7 +60,7 @@ modification follow. 0. Definitions. - "This License" refers to version 3 of the GNU General Public License. + "This License" refers to version 3 of the GNU Affero General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. @@ -549,35 +537,45 @@ to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. - 13. Use with the GNU Affero General Public License. + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed -under version 3 of the GNU Affero General Public License into a single +under version 3 of the GNU General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, -but the special requirements of the GNU Affero General Public License, -section 13, concerning interaction through a network will apply to the -combination as such. +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of -the GNU General Public License from time to time. Such new versions will -be similar in spirit to the present version, but may differ in detail to +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the -Program specifies that a certain numbered version of the GNU General +Program specifies that a certain numbered version of the GNU Affero General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the -GNU General Public License, you may choose any version ever published +GNU Affero General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future -versions of the GNU General Public License can be used, that proxy's +versions of the GNU Affero General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. @@ -635,45 +633,34 @@ the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by + it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. + GNU Affero General Public License for more details. - You should have received a copy of the GNU General Public License + You should have received a copy of the GNU Affero General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. - If the program does terminal interaction, make it output a short -notice like this when it starts in an interactive mode: - - Copyright (C) - This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. - This is free software, and you are welcome to redistribute it - under certain conditions; type `show c' for details. - -The hypothetical commands `show w' and `show c' should show the appropriate -parts of the General Public License. Of course, your program's commands -might be different; for a GUI interface, you would use an "about box". + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. -For more information on this, and how to apply and follow the GNU GPL, see +For more information on this, and how to apply and follow the GNU AGPL, see . - The GNU General Public License does not permit incorporating your program -into proprietary programs. If your program is a subroutine library, you -may consider it more useful to permit linking proprietary applications with -the library. If this is what you want to do, use the GNU Lesser General -Public License instead of this License. But first, please read -. - -Additional permission under GNU GPL version 3 section 7 +Additional permission under GNU AGPL version 3 section 7 If you modify this program, or any covered work, by linking or combining it with the OpenSSL project's OpenSSL library (or a diff --git a/LICENSES/GPL-3.0-or-later-OpenSSL.txt b/LICENSES/AGPL-3.0-only-OpenSSL.txt similarity index 86% rename from LICENSES/GPL-3.0-or-later-OpenSSL.txt rename to LICENSES/AGPL-3.0-only-OpenSSL.txt index dbc3698..c3ef0cb 100644 --- a/LICENSES/GPL-3.0-or-later-OpenSSL.txt +++ b/LICENSES/AGPL-3.0-only-OpenSSL.txt @@ -1,5 +1,5 @@ - GNU GENERAL PUBLIC LICENSE - Version 3, 29 June 2007 + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies @@ -7,17 +7,15 @@ Preamble - The GNU General Public License is a free, copyleft license for -software and other kinds of works. + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, -the GNU General Public License is intended to guarantee your freedom to +our General Public Licenses are intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free -software for all its users. We, the Free Software Foundation, use the -GNU General Public License for most of our software; it applies also to -any other work released this way by its authors. You can apply it to -your programs, too. +software for all its users. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you @@ -26,44 +24,34 @@ them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. - To protect your rights, we need to prevent others from denying you -these rights or asking you to surrender the rights. Therefore, you have -certain responsibilities if you distribute copies of the software, or if -you modify it: responsibilities to respect the freedom of others. + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. - For example, if you distribute copies of such a program, whether -gratis or for a fee, you must pass on to the recipients the same -freedoms that you received. You must make sure that they, too, receive -or can get the source code. And you must show them these terms so they -know their rights. + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. - Developers that use the GNU GPL protect your rights with two steps: -(1) assert copyright on the software, and (2) offer you this License -giving you legal permission to copy, distribute and/or modify it. + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. - For the developers' and authors' protection, the GPL clearly explains -that there is no warranty for this free software. For both users' and -authors' sake, the GPL requires that modified versions be marked as -changed, so that their problems will not be attributed erroneously to -authors of previous versions. - - Some devices are designed to deny users access to install or run -modified versions of the software inside them, although the manufacturer -can do so. This is fundamentally incompatible with the aim of -protecting users' freedom to change the software. The systematic -pattern of such abuse occurs in the area of products for individuals to -use, which is precisely where it is most unacceptable. Therefore, we -have designed this version of the GPL to prohibit the practice for those -products. If such problems arise substantially in other domains, we -stand ready to extend this provision to those domains in future versions -of the GPL, as needed to protect the freedom of users. - - Finally, every program is threatened constantly by software patents. -States should not allow patents to restrict development and use of -software on general-purpose computers, but in those that do, we wish to -avoid the special danger that patents applied to a free program could -make it effectively proprietary. To prevent this, the GPL assures that -patents cannot be used to render the program non-free. + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. The precise terms and conditions for copying, distribution and modification follow. @@ -72,7 +60,7 @@ modification follow. 0. Definitions. - "This License" refers to version 3 of the GNU General Public License. + "This License" refers to version 3 of the GNU Affero General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. @@ -549,35 +537,45 @@ to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. - 13. Use with the GNU Affero General Public License. + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed -under version 3 of the GNU Affero General Public License into a single +under version 3 of the GNU General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, -but the special requirements of the GNU Affero General Public License, -section 13, concerning interaction through a network will apply to the -combination as such. +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of -the GNU General Public License from time to time. Such new versions will -be similar in spirit to the present version, but may differ in detail to +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the -Program specifies that a certain numbered version of the GNU General +Program specifies that a certain numbered version of the GNU Affero General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the -GNU General Public License, you may choose any version ever published +GNU Affero General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future -versions of the GNU General Public License can be used, that proxy's +versions of the GNU Affero General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. @@ -635,45 +633,34 @@ the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by + it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. + GNU Affero General Public License for more details. - You should have received a copy of the GNU General Public License + You should have received a copy of the GNU Affero General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. - If the program does terminal interaction, make it output a short -notice like this when it starts in an interactive mode: - - Copyright (C) - This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. - This is free software, and you are welcome to redistribute it - under certain conditions; type `show c' for details. - -The hypothetical commands `show w' and `show c' should show the appropriate -parts of the General Public License. Of course, your program's commands -might be different; for a GUI interface, you would use an "about box". + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. -For more information on this, and how to apply and follow the GNU GPL, see +For more information on this, and how to apply and follow the GNU AGPL, see . - The GNU General Public License does not permit incorporating your program -into proprietary programs. If your program is a subroutine library, you -may consider it more useful to permit linking proprietary applications with -the library. If this is what you want to do, use the GNU Lesser General -Public License instead of this License. But first, please read -. - -Additional permission under GNU GPL version 3 section 7 +Additional permission under GNU AGPL version 3 section 7 If you modify this program, or any covered work, by linking or combining it with the OpenSSL project's OpenSSL library (or a diff --git a/README.md b/README.md index 3f170ad..bbcbd75 100644 --- a/README.md +++ b/README.md @@ -95,23 +95,21 @@ extremely helpful information about FEC and error correction. ## About -Created by Florian Märkl. +Created by Florian Märkl This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version, with the additional exemption -that compiling, linking, and/or using OpenSSL is allowed. +it under the terms of the GNU Affero General Public License version 3 +as published by the Free Software Foundation. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. +GNU Affero General Public License for more details. -You should have received a copy of the GNU General Public License +You should have received a copy of the GNU Affero General Public License along with this program. If not, see . -Additional permission under GNU GPL version 3 section 7 +Additional permission under GNU AGPL version 3 section 7 If you modify this program, or any covered work, by linking or combining it with the OpenSSL project's OpenSSL library (or a diff --git a/android/app/src/main/cpp/audio-decoder.c b/android/app/src/main/cpp/audio-decoder.c index 814bd83..35f97e9 100644 --- a/android/app/src/main/cpp/audio-decoder.c +++ b/android/app/src/main/cpp/audio-decoder.c @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL #include "audio-decoder.h" diff --git a/android/app/src/main/cpp/audio-decoder.h b/android/app/src/main/cpp/audio-decoder.h index 1bca6cd..c365cd2 100644 --- a/android/app/src/main/cpp/audio-decoder.h +++ b/android/app/src/main/cpp/audio-decoder.h @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL #ifndef CHIAKI_JNI_AUDIO_DECODER_H #define CHIAKI_JNI_AUDIO_DECODER_H diff --git a/android/app/src/main/cpp/audio-output.cpp b/android/app/src/main/cpp/audio-output.cpp index ccd9733..53157f1 100644 --- a/android/app/src/main/cpp/audio-output.cpp +++ b/android/app/src/main/cpp/audio-output.cpp @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL #include "audio-output.h" diff --git a/android/app/src/main/cpp/audio-output.h b/android/app/src/main/cpp/audio-output.h index 3cad981..b521ebe 100644 --- a/android/app/src/main/cpp/audio-output.h +++ b/android/app/src/main/cpp/audio-output.h @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL #ifndef CHIAKI_JNI_AUDIO_OUTPUT_H #define CHIAKI_JNI_AUDIO_OUTPUT_H diff --git a/android/app/src/main/cpp/chiaki-jni.c b/android/app/src/main/cpp/chiaki-jni.c index b970ddc..21fab93 100644 --- a/android/app/src/main/cpp/chiaki-jni.c +++ b/android/app/src/main/cpp/chiaki-jni.c @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL #include diff --git a/android/app/src/main/cpp/chiaki-jni.h b/android/app/src/main/cpp/chiaki-jni.h index 3c802ec..99383f4 100644 --- a/android/app/src/main/cpp/chiaki-jni.h +++ b/android/app/src/main/cpp/chiaki-jni.h @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL #ifndef CHIAKI_JNI_H #define CHIAKI_JNI_H diff --git a/android/app/src/main/cpp/circular-buf.hpp b/android/app/src/main/cpp/circular-buf.hpp index bddc8b6..21baf9c 100644 --- a/android/app/src/main/cpp/circular-buf.hpp +++ b/android/app/src/main/cpp/circular-buf.hpp @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL #ifndef CHIAKI_JNI_CIRCULARBUF_HPP #define CHIAKI_JNI_CIRCULARBUF_HPP diff --git a/android/app/src/main/cpp/log.c b/android/app/src/main/cpp/log.c index 8962d10..cbb3c7e 100644 --- a/android/app/src/main/cpp/log.c +++ b/android/app/src/main/cpp/log.c @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL #include "log.h" diff --git a/android/app/src/main/cpp/log.h b/android/app/src/main/cpp/log.h index 6edf769..19caf35 100644 --- a/android/app/src/main/cpp/log.h +++ b/android/app/src/main/cpp/log.h @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL #ifndef CHIAKI_JNI_LOG_H #define CHIAKI_JNI_LOG_H diff --git a/android/app/src/main/cpp/video-decoder.c b/android/app/src/main/cpp/video-decoder.c index 534cc89..43c9bc4 100644 --- a/android/app/src/main/cpp/video-decoder.c +++ b/android/app/src/main/cpp/video-decoder.c @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL #include "video-decoder.h" diff --git a/android/app/src/main/cpp/video-decoder.h b/android/app/src/main/cpp/video-decoder.h index f7ecdc8..0578e7f 100644 --- a/android/app/src/main/cpp/video-decoder.h +++ b/android/app/src/main/cpp/video-decoder.h @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL #ifndef CHIAKI_JNI_VIDEO_DECODER_H #define CHIAKI_JNI_VIDEO_DECODER_H diff --git a/android/app/src/main/java/com/metallic/chiaki/common/AppDatabase.kt b/android/app/src/main/java/com/metallic/chiaki/common/AppDatabase.kt index 71db6c2..5a794f1 100644 --- a/android/app/src/main/java/com/metallic/chiaki/common/AppDatabase.kt +++ b/android/app/src/main/java/com/metallic/chiaki/common/AppDatabase.kt @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL package com.metallic.chiaki.common diff --git a/android/app/src/main/java/com/metallic/chiaki/common/DisplayHost.kt b/android/app/src/main/java/com/metallic/chiaki/common/DisplayHost.kt index 658785c..f930f39 100644 --- a/android/app/src/main/java/com/metallic/chiaki/common/DisplayHost.kt +++ b/android/app/src/main/java/com/metallic/chiaki/common/DisplayHost.kt @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL package com.metallic.chiaki.common diff --git a/android/app/src/main/java/com/metallic/chiaki/common/LogManager.kt b/android/app/src/main/java/com/metallic/chiaki/common/LogManager.kt index 33683b3..22c4c0c 100644 --- a/android/app/src/main/java/com/metallic/chiaki/common/LogManager.kt +++ b/android/app/src/main/java/com/metallic/chiaki/common/LogManager.kt @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL package com.metallic.chiaki.common diff --git a/android/app/src/main/java/com/metallic/chiaki/common/MacAddress.kt b/android/app/src/main/java/com/metallic/chiaki/common/MacAddress.kt index c0c9181..7927d77 100644 --- a/android/app/src/main/java/com/metallic/chiaki/common/MacAddress.kt +++ b/android/app/src/main/java/com/metallic/chiaki/common/MacAddress.kt @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL package com.metallic.chiaki.common diff --git a/android/app/src/main/java/com/metallic/chiaki/common/ManualHost.kt b/android/app/src/main/java/com/metallic/chiaki/common/ManualHost.kt index 3b5784e..5825000 100644 --- a/android/app/src/main/java/com/metallic/chiaki/common/ManualHost.kt +++ b/android/app/src/main/java/com/metallic/chiaki/common/ManualHost.kt @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL package com.metallic.chiaki.common diff --git a/android/app/src/main/java/com/metallic/chiaki/common/Preferences.kt b/android/app/src/main/java/com/metallic/chiaki/common/Preferences.kt index a7766b9..3da0a22 100644 --- a/android/app/src/main/java/com/metallic/chiaki/common/Preferences.kt +++ b/android/app/src/main/java/com/metallic/chiaki/common/Preferences.kt @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL package com.metallic.chiaki.common diff --git a/android/app/src/main/java/com/metallic/chiaki/common/RegisteredHost.kt b/android/app/src/main/java/com/metallic/chiaki/common/RegisteredHost.kt index 9e679fe..203ff75 100644 --- a/android/app/src/main/java/com/metallic/chiaki/common/RegisteredHost.kt +++ b/android/app/src/main/java/com/metallic/chiaki/common/RegisteredHost.kt @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL package com.metallic.chiaki.common diff --git a/android/app/src/main/java/com/metallic/chiaki/common/SerializedSettings.kt b/android/app/src/main/java/com/metallic/chiaki/common/SerializedSettings.kt index c85bffd..eb6c9e4 100644 --- a/android/app/src/main/java/com/metallic/chiaki/common/SerializedSettings.kt +++ b/android/app/src/main/java/com/metallic/chiaki/common/SerializedSettings.kt @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL package com.metallic.chiaki.common diff --git a/android/app/src/main/java/com/metallic/chiaki/common/ext/RevealActivity.kt b/android/app/src/main/java/com/metallic/chiaki/common/ext/RevealActivity.kt index b1d25ac..321e0bd 100644 --- a/android/app/src/main/java/com/metallic/chiaki/common/ext/RevealActivity.kt +++ b/android/app/src/main/java/com/metallic/chiaki/common/ext/RevealActivity.kt @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL package com.metallic.chiaki.common.ext diff --git a/android/app/src/main/java/com/metallic/chiaki/common/ext/RxLiveData.kt b/android/app/src/main/java/com/metallic/chiaki/common/ext/RxLiveData.kt index 8c1d0d5..bd98458 100644 --- a/android/app/src/main/java/com/metallic/chiaki/common/ext/RxLiveData.kt +++ b/android/app/src/main/java/com/metallic/chiaki/common/ext/RxLiveData.kt @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL package com.metallic.chiaki.common.ext diff --git a/android/app/src/main/java/com/metallic/chiaki/common/ext/StringHex.kt b/android/app/src/main/java/com/metallic/chiaki/common/ext/StringHex.kt index 73011e1..6212ddc 100644 --- a/android/app/src/main/java/com/metallic/chiaki/common/ext/StringHex.kt +++ b/android/app/src/main/java/com/metallic/chiaki/common/ext/StringHex.kt @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL package com.metallic.chiaki.common.ext diff --git a/android/app/src/main/java/com/metallic/chiaki/common/ext/ViewGroupInflate.kt b/android/app/src/main/java/com/metallic/chiaki/common/ext/ViewGroupInflate.kt index 887d509..0b613cc 100644 --- a/android/app/src/main/java/com/metallic/chiaki/common/ext/ViewGroupInflate.kt +++ b/android/app/src/main/java/com/metallic/chiaki/common/ext/ViewGroupInflate.kt @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL package com.metallic.chiaki.common.ext diff --git a/android/app/src/main/java/com/metallic/chiaki/discovery/DiscoveryManager.kt b/android/app/src/main/java/com/metallic/chiaki/discovery/DiscoveryManager.kt index 146b779..1579d83 100644 --- a/android/app/src/main/java/com/metallic/chiaki/discovery/DiscoveryManager.kt +++ b/android/app/src/main/java/com/metallic/chiaki/discovery/DiscoveryManager.kt @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL package com.metallic.chiaki.discovery diff --git a/android/app/src/main/java/com/metallic/chiaki/main/DisplayHostRecyclerViewAdapter.kt b/android/app/src/main/java/com/metallic/chiaki/main/DisplayHostRecyclerViewAdapter.kt index 22ceae6..4dd9a69 100644 --- a/android/app/src/main/java/com/metallic/chiaki/main/DisplayHostRecyclerViewAdapter.kt +++ b/android/app/src/main/java/com/metallic/chiaki/main/DisplayHostRecyclerViewAdapter.kt @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL package com.metallic.chiaki.main diff --git a/android/app/src/main/java/com/metallic/chiaki/main/FloatingActionButtonBackgroundBehavior.kt b/android/app/src/main/java/com/metallic/chiaki/main/FloatingActionButtonBackgroundBehavior.kt index 0776723..3babfa9 100644 --- a/android/app/src/main/java/com/metallic/chiaki/main/FloatingActionButtonBackgroundBehavior.kt +++ b/android/app/src/main/java/com/metallic/chiaki/main/FloatingActionButtonBackgroundBehavior.kt @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL package com.metallic.chiaki.main diff --git a/android/app/src/main/java/com/metallic/chiaki/main/FloatingActionButtonSpeedDialBehavior.kt b/android/app/src/main/java/com/metallic/chiaki/main/FloatingActionButtonSpeedDialBehavior.kt index 20853fd..59a2003 100644 --- a/android/app/src/main/java/com/metallic/chiaki/main/FloatingActionButtonSpeedDialBehavior.kt +++ b/android/app/src/main/java/com/metallic/chiaki/main/FloatingActionButtonSpeedDialBehavior.kt @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL package com.metallic.chiaki.main diff --git a/android/app/src/main/java/com/metallic/chiaki/main/MainActivity.kt b/android/app/src/main/java/com/metallic/chiaki/main/MainActivity.kt index 12eb19b..779c789 100644 --- a/android/app/src/main/java/com/metallic/chiaki/main/MainActivity.kt +++ b/android/app/src/main/java/com/metallic/chiaki/main/MainActivity.kt @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL package com.metallic.chiaki.main diff --git a/android/app/src/main/java/com/metallic/chiaki/main/MainViewModel.kt b/android/app/src/main/java/com/metallic/chiaki/main/MainViewModel.kt index 95d4eee..6224be5 100644 --- a/android/app/src/main/java/com/metallic/chiaki/main/MainViewModel.kt +++ b/android/app/src/main/java/com/metallic/chiaki/main/MainViewModel.kt @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL package com.metallic.chiaki.main diff --git a/android/app/src/main/java/com/metallic/chiaki/manualconsole/EditManualConsoleActivity.kt b/android/app/src/main/java/com/metallic/chiaki/manualconsole/EditManualConsoleActivity.kt index 143f712..d61fe30 100644 --- a/android/app/src/main/java/com/metallic/chiaki/manualconsole/EditManualConsoleActivity.kt +++ b/android/app/src/main/java/com/metallic/chiaki/manualconsole/EditManualConsoleActivity.kt @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL package com.metallic.chiaki.manualconsole diff --git a/android/app/src/main/java/com/metallic/chiaki/manualconsole/EditManualConsoleViewModel.kt b/android/app/src/main/java/com/metallic/chiaki/manualconsole/EditManualConsoleViewModel.kt index da8b5fe..160e4a3 100644 --- a/android/app/src/main/java/com/metallic/chiaki/manualconsole/EditManualConsoleViewModel.kt +++ b/android/app/src/main/java/com/metallic/chiaki/manualconsole/EditManualConsoleViewModel.kt @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL package com.metallic.chiaki.manualconsole diff --git a/android/app/src/main/java/com/metallic/chiaki/regist/ChiakiRxLog.kt b/android/app/src/main/java/com/metallic/chiaki/regist/ChiakiRxLog.kt index 536fab2..f43695f 100644 --- a/android/app/src/main/java/com/metallic/chiaki/regist/ChiakiRxLog.kt +++ b/android/app/src/main/java/com/metallic/chiaki/regist/ChiakiRxLog.kt @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL package com.metallic.chiaki.regist diff --git a/android/app/src/main/java/com/metallic/chiaki/regist/RegistActivity.kt b/android/app/src/main/java/com/metallic/chiaki/regist/RegistActivity.kt index 4762119..4f5af54 100644 --- a/android/app/src/main/java/com/metallic/chiaki/regist/RegistActivity.kt +++ b/android/app/src/main/java/com/metallic/chiaki/regist/RegistActivity.kt @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL package com.metallic.chiaki.regist diff --git a/android/app/src/main/java/com/metallic/chiaki/regist/RegistExecuteActivity.kt b/android/app/src/main/java/com/metallic/chiaki/regist/RegistExecuteActivity.kt index 7bf79ee..88e0f6b 100644 --- a/android/app/src/main/java/com/metallic/chiaki/regist/RegistExecuteActivity.kt +++ b/android/app/src/main/java/com/metallic/chiaki/regist/RegistExecuteActivity.kt @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL package com.metallic.chiaki.regist diff --git a/android/app/src/main/java/com/metallic/chiaki/regist/RegistExecuteViewModel.kt b/android/app/src/main/java/com/metallic/chiaki/regist/RegistExecuteViewModel.kt index 02593af..9d8a0d5 100644 --- a/android/app/src/main/java/com/metallic/chiaki/regist/RegistExecuteViewModel.kt +++ b/android/app/src/main/java/com/metallic/chiaki/regist/RegistExecuteViewModel.kt @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL package com.metallic.chiaki.regist diff --git a/android/app/src/main/java/com/metallic/chiaki/regist/RegistViewModel.kt b/android/app/src/main/java/com/metallic/chiaki/regist/RegistViewModel.kt index 297e65a..d99778e 100644 --- a/android/app/src/main/java/com/metallic/chiaki/regist/RegistViewModel.kt +++ b/android/app/src/main/java/com/metallic/chiaki/regist/RegistViewModel.kt @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL package com.metallic.chiaki.regist diff --git a/android/app/src/main/java/com/metallic/chiaki/session/StreamSession.kt b/android/app/src/main/java/com/metallic/chiaki/session/StreamSession.kt index f76d37f..059840c 100644 --- a/android/app/src/main/java/com/metallic/chiaki/session/StreamSession.kt +++ b/android/app/src/main/java/com/metallic/chiaki/session/StreamSession.kt @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL package com.metallic.chiaki.session diff --git a/android/app/src/main/java/com/metallic/chiaki/settings/ItemTouchSwipeCallback.kt b/android/app/src/main/java/com/metallic/chiaki/settings/ItemTouchSwipeCallback.kt index d4f2125..7168bce 100644 --- a/android/app/src/main/java/com/metallic/chiaki/settings/ItemTouchSwipeCallback.kt +++ b/android/app/src/main/java/com/metallic/chiaki/settings/ItemTouchSwipeCallback.kt @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL package com.metallic.chiaki.settings diff --git a/android/app/src/main/java/com/metallic/chiaki/settings/SettingsActivity.kt b/android/app/src/main/java/com/metallic/chiaki/settings/SettingsActivity.kt index 9f1c712..3a87e1b 100644 --- a/android/app/src/main/java/com/metallic/chiaki/settings/SettingsActivity.kt +++ b/android/app/src/main/java/com/metallic/chiaki/settings/SettingsActivity.kt @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL package com.metallic.chiaki.settings diff --git a/android/app/src/main/java/com/metallic/chiaki/settings/SettingsFragment.kt b/android/app/src/main/java/com/metallic/chiaki/settings/SettingsFragment.kt index 0558708..2001eed 100644 --- a/android/app/src/main/java/com/metallic/chiaki/settings/SettingsFragment.kt +++ b/android/app/src/main/java/com/metallic/chiaki/settings/SettingsFragment.kt @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL package com.metallic.chiaki.settings diff --git a/android/app/src/main/java/com/metallic/chiaki/settings/SettingsLogsAdapter.kt b/android/app/src/main/java/com/metallic/chiaki/settings/SettingsLogsAdapter.kt index 93821e3..46c8904 100644 --- a/android/app/src/main/java/com/metallic/chiaki/settings/SettingsLogsAdapter.kt +++ b/android/app/src/main/java/com/metallic/chiaki/settings/SettingsLogsAdapter.kt @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL package com.metallic.chiaki.settings diff --git a/android/app/src/main/java/com/metallic/chiaki/settings/SettingsLogsFragment.kt b/android/app/src/main/java/com/metallic/chiaki/settings/SettingsLogsFragment.kt index c0c2e3d..07e1e3f 100644 --- a/android/app/src/main/java/com/metallic/chiaki/settings/SettingsLogsFragment.kt +++ b/android/app/src/main/java/com/metallic/chiaki/settings/SettingsLogsFragment.kt @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL package com.metallic.chiaki.settings diff --git a/android/app/src/main/java/com/metallic/chiaki/settings/SettingsLogsViewModel.kt b/android/app/src/main/java/com/metallic/chiaki/settings/SettingsLogsViewModel.kt index 97cd214..89a58de 100644 --- a/android/app/src/main/java/com/metallic/chiaki/settings/SettingsLogsViewModel.kt +++ b/android/app/src/main/java/com/metallic/chiaki/settings/SettingsLogsViewModel.kt @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL package com.metallic.chiaki.settings diff --git a/android/app/src/main/java/com/metallic/chiaki/settings/SettingsRegisteredHostsAdapter.kt b/android/app/src/main/java/com/metallic/chiaki/settings/SettingsRegisteredHostsAdapter.kt index 4edeed7..b92d7f7 100644 --- a/android/app/src/main/java/com/metallic/chiaki/settings/SettingsRegisteredHostsAdapter.kt +++ b/android/app/src/main/java/com/metallic/chiaki/settings/SettingsRegisteredHostsAdapter.kt @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL package com.metallic.chiaki.settings diff --git a/android/app/src/main/java/com/metallic/chiaki/settings/SettingsRegisteredHostsFragment.kt b/android/app/src/main/java/com/metallic/chiaki/settings/SettingsRegisteredHostsFragment.kt index f83164d..7c69266 100644 --- a/android/app/src/main/java/com/metallic/chiaki/settings/SettingsRegisteredHostsFragment.kt +++ b/android/app/src/main/java/com/metallic/chiaki/settings/SettingsRegisteredHostsFragment.kt @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL package com.metallic.chiaki.settings diff --git a/android/app/src/main/java/com/metallic/chiaki/settings/SettingsRegisteredHostsViewModel.kt b/android/app/src/main/java/com/metallic/chiaki/settings/SettingsRegisteredHostsViewModel.kt index 1df6662..a00e897 100644 --- a/android/app/src/main/java/com/metallic/chiaki/settings/SettingsRegisteredHostsViewModel.kt +++ b/android/app/src/main/java/com/metallic/chiaki/settings/SettingsRegisteredHostsViewModel.kt @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL package com.metallic.chiaki.settings diff --git a/android/app/src/main/java/com/metallic/chiaki/settings/SettingsViewModel.kt b/android/app/src/main/java/com/metallic/chiaki/settings/SettingsViewModel.kt index 1811eb8..3df021c 100644 --- a/android/app/src/main/java/com/metallic/chiaki/settings/SettingsViewModel.kt +++ b/android/app/src/main/java/com/metallic/chiaki/settings/SettingsViewModel.kt @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL package com.metallic.chiaki.settings diff --git a/android/app/src/main/java/com/metallic/chiaki/stream/StreamActivity.kt b/android/app/src/main/java/com/metallic/chiaki/stream/StreamActivity.kt index 8d3412f..67d425b 100644 --- a/android/app/src/main/java/com/metallic/chiaki/stream/StreamActivity.kt +++ b/android/app/src/main/java/com/metallic/chiaki/stream/StreamActivity.kt @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL package com.metallic.chiaki.stream diff --git a/android/app/src/main/java/com/metallic/chiaki/stream/StreamViewModel.kt b/android/app/src/main/java/com/metallic/chiaki/stream/StreamViewModel.kt index 8709090..ebaaf80 100644 --- a/android/app/src/main/java/com/metallic/chiaki/stream/StreamViewModel.kt +++ b/android/app/src/main/java/com/metallic/chiaki/stream/StreamViewModel.kt @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL package com.metallic.chiaki.stream diff --git a/android/app/src/main/java/com/metallic/chiaki/touchcontrols/AnalogStickView.kt b/android/app/src/main/java/com/metallic/chiaki/touchcontrols/AnalogStickView.kt index f1aa815..0c0a310 100644 --- a/android/app/src/main/java/com/metallic/chiaki/touchcontrols/AnalogStickView.kt +++ b/android/app/src/main/java/com/metallic/chiaki/touchcontrols/AnalogStickView.kt @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL package com.metallic.chiaki.touchcontrols diff --git a/android/app/src/main/java/com/metallic/chiaki/touchcontrols/ButtonView.kt b/android/app/src/main/java/com/metallic/chiaki/touchcontrols/ButtonView.kt index d709bc5..b2eba6b 100644 --- a/android/app/src/main/java/com/metallic/chiaki/touchcontrols/ButtonView.kt +++ b/android/app/src/main/java/com/metallic/chiaki/touchcontrols/ButtonView.kt @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL package com.metallic.chiaki.touchcontrols diff --git a/android/app/src/main/java/com/metallic/chiaki/touchcontrols/ControlsBackgroundView.kt b/android/app/src/main/java/com/metallic/chiaki/touchcontrols/ControlsBackgroundView.kt index 96a3fc7..3edbd86 100644 --- a/android/app/src/main/java/com/metallic/chiaki/touchcontrols/ControlsBackgroundView.kt +++ b/android/app/src/main/java/com/metallic/chiaki/touchcontrols/ControlsBackgroundView.kt @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL package com.metallic.chiaki.touchcontrols diff --git a/android/app/src/main/java/com/metallic/chiaki/touchcontrols/DPadView.kt b/android/app/src/main/java/com/metallic/chiaki/touchcontrols/DPadView.kt index eb6f52d..9386e8f 100644 --- a/android/app/src/main/java/com/metallic/chiaki/touchcontrols/DPadView.kt +++ b/android/app/src/main/java/com/metallic/chiaki/touchcontrols/DPadView.kt @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL package com.metallic.chiaki.touchcontrols diff --git a/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchControlsFragment.kt b/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchControlsFragment.kt index ece97b2..9c4ca51 100644 --- a/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchControlsFragment.kt +++ b/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchControlsFragment.kt @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL package com.metallic.chiaki.touchcontrols diff --git a/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchTracker.kt b/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchTracker.kt index cb65c33..01d6869 100644 --- a/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchTracker.kt +++ b/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchTracker.kt @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL package com.metallic.chiaki.touchcontrols diff --git a/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchpadOnlyFragment.kt b/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchpadOnlyFragment.kt index 36c8744..610c099 100644 --- a/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchpadOnlyFragment.kt +++ b/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchpadOnlyFragment.kt @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL package com.metallic.chiaki.touchcontrols diff --git a/android/app/src/main/java/com/metallic/chiaki/touchcontrols/Vector.kt b/android/app/src/main/java/com/metallic/chiaki/touchcontrols/Vector.kt index dc3bed7..36216e8 100644 --- a/android/app/src/main/java/com/metallic/chiaki/touchcontrols/Vector.kt +++ b/android/app/src/main/java/com/metallic/chiaki/touchcontrols/Vector.kt @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL package com.metallic.chiaki.touchcontrols diff --git a/cli/include/chiaki-cli.h b/cli/include/chiaki-cli.h index 852441d..91ee0dd 100644 --- a/cli/include/chiaki-cli.h +++ b/cli/include/chiaki-cli.h @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL #ifndef CHIAKI_CHIAKI_CLI_H #define CHIAKI_CHIAKI_CLI_H diff --git a/cli/src/discover.c b/cli/src/discover.c index 0f2f152..3ebb9d1 100644 --- a/cli/src/discover.c +++ b/cli/src/discover.c @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL #include diff --git a/cli/src/main.c b/cli/src/main.c index 05f0b4c..41c157e 100644 --- a/cli/src/main.c +++ b/cli/src/main.c @@ -1,19 +1,4 @@ -/* -* This file is part of Chiaki. -* -* Chiaki is free software: you can redistribute it and/or modify -* it under the terms of the GNU General Public License as published by -* the Free Software Foundation, either version 3 of the License, or -* (at your option) any later version. -* -* Chiaki is distributed in the hope that it will be useful, -* but WITHOUT ANY WARRANTY; without even the implied warranty of -* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -* GNU General Public License for more details. -* -* You should have received a copy of the GNU General Public License -* along with Chiaki. If not, see . -*/ +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL #include diff --git a/cli/src/wakeup.c b/cli/src/wakeup.c index 7c6213f..cc7dcde 100644 --- a/cli/src/wakeup.c +++ b/cli/src/wakeup.c @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL #include diff --git a/gui/CMakeLists.txt b/gui/CMakeLists.txt index 2932883..a61b763 100644 --- a/gui/CMakeLists.txt +++ b/gui/CMakeLists.txt @@ -93,7 +93,7 @@ set_target_properties(chiaki PROPERTIES MACOSX_BUNDLE_INFO_PLIST "${CMAKE_CURRENT_SOURCE_DIR}/MacOSXBundleInfo.plist.in" MACOSX_BUNDLE_BUNDLE_NAME Chiaki MACOSX_BUNDLE_BUNDLE_VERSION ${CHIAKI_VERSION} - MACOSX_BUNDLE_COPYRIGHT "thestr4ng3r (GPLv3)" + MACOSX_BUNDLE_COPYRIGHT "thestr4ng3r (AGPLv3)" MACOSX_BUNDLE_GUI_IDENTIFIER "org.chiaki.chiaki" MACOSX_BUNDLE_ICON_FILE chiaki.icns RESOURCE "${RESOURCE_FILES}") diff --git a/gui/include/avopenglframeuploader.h b/gui/include/avopenglframeuploader.h index 12c85ef..51fd2d9 100644 --- a/gui/include/avopenglframeuploader.h +++ b/gui/include/avopenglframeuploader.h @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL #ifndef CHIAKI_AVOPENGLFRAMEUPLOADER_H #define CHIAKI_AVOPENGLFRAMEUPLOADER_H diff --git a/gui/include/avopenglwidget.h b/gui/include/avopenglwidget.h index 179be64..328b74f 100644 --- a/gui/include/avopenglwidget.h +++ b/gui/include/avopenglwidget.h @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL #ifndef CHIAKI_AVOPENGLWIDGET_H #define CHIAKI_AVOPENGLWIDGET_H diff --git a/gui/include/controllermanager.h b/gui/include/controllermanager.h index b409a50..6ec6efb 100644 --- a/gui/include/controllermanager.h +++ b/gui/include/controllermanager.h @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL #ifndef CHIAKI_CONTROLLERMANAGER_H #define CHIAKI_CONTROLLERMANAGER_H diff --git a/gui/include/discoverymanager.h b/gui/include/discoverymanager.h index ba0cdf4..67093de 100644 --- a/gui/include/discoverymanager.h +++ b/gui/include/discoverymanager.h @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL #ifndef CHIAKI_DISCOVERYMANAGER_H #define CHIAKI_DISCOVERYMANAGER_H diff --git a/gui/include/dynamicgridwidget.h b/gui/include/dynamicgridwidget.h index 517fe13..6a9330d 100644 --- a/gui/include/dynamicgridwidget.h +++ b/gui/include/dynamicgridwidget.h @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL #ifndef CHIAKI_DYNAMICGRIDWIDGET_H #define CHIAKI_DYNAMICGRIDWIDGET_H diff --git a/gui/include/exception.h b/gui/include/exception.h index fd09846..2ffe45c 100644 --- a/gui/include/exception.h +++ b/gui/include/exception.h @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL #ifndef CHIAKI_EXCEPTION_H #define CHIAKI_EXCEPTION_H diff --git a/gui/include/host.h b/gui/include/host.h index 1394e40..0e403ce 100644 --- a/gui/include/host.h +++ b/gui/include/host.h @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL #ifndef CHIAKI_HOST_H #define CHIAKI_HOST_H diff --git a/gui/include/loginpindialog.h b/gui/include/loginpindialog.h index e794e1a..cc3a411 100644 --- a/gui/include/loginpindialog.h +++ b/gui/include/loginpindialog.h @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL #ifndef CHIAKI_LOGINPINDIALOG_H #define CHIAKI_LOGINPINDIALOG_H diff --git a/gui/include/mainwindow.h b/gui/include/mainwindow.h index 20e2d45..715c7d9 100644 --- a/gui/include/mainwindow.h +++ b/gui/include/mainwindow.h @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL #ifndef CHIAKI_MAINWINDOW_H #define CHIAKI_MAINWINDOW_H diff --git a/gui/include/manualhostdialog.h b/gui/include/manualhostdialog.h index b0bd433..b56cc8e 100644 --- a/gui/include/manualhostdialog.h +++ b/gui/include/manualhostdialog.h @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL #ifndef CHIAKI_MANUALHOSTDIALOG_H #define CHIAKI_MANUALHOSTDIALOG_H diff --git a/gui/include/registdialog.h b/gui/include/registdialog.h index 45a5a1d..06098ee 100644 --- a/gui/include/registdialog.h +++ b/gui/include/registdialog.h @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL #ifndef CHIAKI_REGISTDIALOG_H #define CHIAKI_REGISTDIALOG_H diff --git a/gui/include/servericonwidget.h b/gui/include/servericonwidget.h index a0c2a1e..1a8525a 100644 --- a/gui/include/servericonwidget.h +++ b/gui/include/servericonwidget.h @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL #ifndef CHIAKI_SERVERICONWIDGET_H #define CHIAKI_SERVERICONWIDGET_H diff --git a/gui/include/serveritemwidget.h b/gui/include/serveritemwidget.h index 1b04015..24a8ba1 100644 --- a/gui/include/serveritemwidget.h +++ b/gui/include/serveritemwidget.h @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL #ifndef CHIAKI_SERVERITEMWIDGET_H #define CHIAKI_SERVERITEMWIDGET_H diff --git a/gui/include/sessionlog.h b/gui/include/sessionlog.h index db838bc..e498bb3 100644 --- a/gui/include/sessionlog.h +++ b/gui/include/sessionlog.h @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL #ifndef CHIAKI_SESSIONLOG_H #define CHIAKI_SESSIONLOG_H diff --git a/gui/include/settings.h b/gui/include/settings.h index 6b00591..c41ef0b 100644 --- a/gui/include/settings.h +++ b/gui/include/settings.h @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL #ifndef CHIAKI_SETTINGS_H #define CHIAKI_SETTINGS_H diff --git a/gui/include/settingsdialog.h b/gui/include/settingsdialog.h index 27b4ee8..b639b53 100644 --- a/gui/include/settingsdialog.h +++ b/gui/include/settingsdialog.h @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL #ifndef CHIAKI_SETTINGSDIALOG_H #define CHIAKI_SETTINGSDIALOG_H diff --git a/gui/include/settingskeycapturedialog.h b/gui/include/settingskeycapturedialog.h index dbf9cb2..05ddd5f 100644 --- a/gui/include/settingskeycapturedialog.h +++ b/gui/include/settingskeycapturedialog.h @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL #ifndef CHIAKI_SETTINGSKEYCAPTUREDIALOG_H #define CHIAKI_SETTINGSKEYCAPTUREDIALOG_H diff --git a/gui/include/streamsession.h b/gui/include/streamsession.h index 0035aef..3be765d 100644 --- a/gui/include/streamsession.h +++ b/gui/include/streamsession.h @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL #ifndef CHIAKI_STREAMSESSION_H #define CHIAKI_STREAMSESSION_H diff --git a/gui/include/streamwindow.h b/gui/include/streamwindow.h index 78bb530..5c526b0 100644 --- a/gui/include/streamwindow.h +++ b/gui/include/streamwindow.h @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL #ifndef CHIAKI_GUI_STREAMWINDOW_H #define CHIAKI_GUI_STREAMWINDOW_H diff --git a/gui/include/videodecoder.h b/gui/include/videodecoder.h index ebbfd45..f13278a 100644 --- a/gui/include/videodecoder.h +++ b/gui/include/videodecoder.h @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL #ifndef CHIAKI_VIDEODECODER_H #define CHIAKI_VIDEODECODER_H diff --git a/gui/re.chiaki.Chiaki.appdata.xml b/gui/re.chiaki.Chiaki.appdata.xml index a0c6f07..36f3c86 100644 --- a/gui/re.chiaki.Chiaki.appdata.xml +++ b/gui/re.chiaki.Chiaki.appdata.xml @@ -6,7 +6,7 @@ Chiaki Free and Open Source Client for PlayStation 4 Remote Play CC0-1.0 - GPL-3.0 + AGPL-3.0-only Florian Märkl https://github.com/thestr4ng3r/chiaki https://github.com/thestr4ng3r/chiaki/blob/master/README.md#usage diff --git a/gui/src/avopenglframeuploader.cpp b/gui/src/avopenglframeuploader.cpp index e289c53..c57da72 100644 --- a/gui/src/avopenglframeuploader.cpp +++ b/gui/src/avopenglframeuploader.cpp @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL #include #include diff --git a/gui/src/avopenglwidget.cpp b/gui/src/avopenglwidget.cpp index 447f2e6..1aa526a 100644 --- a/gui/src/avopenglwidget.cpp +++ b/gui/src/avopenglwidget.cpp @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL #include #include diff --git a/gui/src/controllermanager.cpp b/gui/src/controllermanager.cpp index cf31a5a..2f9a2aa 100644 --- a/gui/src/controllermanager.cpp +++ b/gui/src/controllermanager.cpp @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL #include diff --git a/gui/src/discoverymanager.cpp b/gui/src/discoverymanager.cpp index 3601d02..f85dc88 100644 --- a/gui/src/discoverymanager.cpp +++ b/gui/src/discoverymanager.cpp @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL #include #include diff --git a/gui/src/dynamicgridwidget.cpp b/gui/src/dynamicgridwidget.cpp index dcd4439..c734f57 100644 --- a/gui/src/dynamicgridwidget.cpp +++ b/gui/src/dynamicgridwidget.cpp @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL #include diff --git a/gui/src/host.cpp b/gui/src/host.cpp index c6d36a4..8a5f8ce 100644 --- a/gui/src/host.cpp +++ b/gui/src/host.cpp @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL #include diff --git a/gui/src/loginpindialog.cpp b/gui/src/loginpindialog.cpp index 1d9ab07..5d1b474 100644 --- a/gui/src/loginpindialog.cpp +++ b/gui/src/loginpindialog.cpp @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL #include diff --git a/gui/src/mainwindow.cpp b/gui/src/mainwindow.cpp index 6c3d8e8..2a10d50 100644 --- a/gui/src/mainwindow.cpp +++ b/gui/src/mainwindow.cpp @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL #include #include diff --git a/gui/src/manualhostdialog.cpp b/gui/src/manualhostdialog.cpp index edcb882..291bb32 100644 --- a/gui/src/manualhostdialog.cpp +++ b/gui/src/manualhostdialog.cpp @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL #include #include diff --git a/gui/src/registdialog.cpp b/gui/src/registdialog.cpp index 39eb713..50c0cad 100644 --- a/gui/src/registdialog.cpp +++ b/gui/src/registdialog.cpp @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL #include #include diff --git a/gui/src/servericonwidget.cpp b/gui/src/servericonwidget.cpp index 3b08917..d001026 100644 --- a/gui/src/servericonwidget.cpp +++ b/gui/src/servericonwidget.cpp @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL #include diff --git a/gui/src/serveritemwidget.cpp b/gui/src/serveritemwidget.cpp index 242d010..f82ae6a 100644 --- a/gui/src/serveritemwidget.cpp +++ b/gui/src/serveritemwidget.cpp @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL #include #include diff --git a/gui/src/sessionlog.cpp b/gui/src/sessionlog.cpp index 3dbb17c..a64c484 100644 --- a/gui/src/sessionlog.cpp +++ b/gui/src/sessionlog.cpp @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL #include #include diff --git a/gui/src/settings.cpp b/gui/src/settings.cpp index 18867f1..c6bcd7b 100644 --- a/gui/src/settings.cpp +++ b/gui/src/settings.cpp @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL #include #include diff --git a/gui/src/settingsdialog.cpp b/gui/src/settingsdialog.cpp index a76170e..73dd350 100644 --- a/gui/src/settingsdialog.cpp +++ b/gui/src/settingsdialog.cpp @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL #include #include @@ -28,9 +28,8 @@ const char * const about_string = "

Chiaki

by thestr4ng3r, version " CHIAKI_VERSION "" "

This program is free software: you can redistribute it and/or modify " - "it under the terms of the GNU General Public License as published by " - "the Free Software Foundation, either version 3 of the License, or " - "(at your option) any later version.

" + "it under the terms of the GNU Affero General Public License version 3 " + "as published by the Free Software Foundation.

" "" "

This program is distributed in the hope that it will be useful, " "but WITHOUT ANY WARRANTY; without even the implied warranty of " diff --git a/gui/src/settingskeycapturedialog.cpp b/gui/src/settingskeycapturedialog.cpp index 2ff1daa..3339813 100644 --- a/gui/src/settingskeycapturedialog.cpp +++ b/gui/src/settingskeycapturedialog.cpp @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL #include "settingskeycapturedialog.h" diff --git a/gui/src/streamsession.cpp b/gui/src/streamsession.cpp index 23f7f80..3c8488b 100644 --- a/gui/src/streamsession.cpp +++ b/gui/src/streamsession.cpp @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL #include #include diff --git a/gui/src/streamwindow.cpp b/gui/src/streamwindow.cpp index e04feda..adc686c 100644 --- a/gui/src/streamwindow.cpp +++ b/gui/src/streamwindow.cpp @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL #include #include diff --git a/gui/src/videodecoder.cpp b/gui/src/videodecoder.cpp index 67a4ed0..48ae4e3 100644 --- a/gui/src/videodecoder.cpp +++ b/gui/src/videodecoder.cpp @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL #include diff --git a/lib/config.h.in b/lib/config.h.in index d06ab98..ecf1ab5 100644 --- a/lib/config.h.in +++ b/lib/config.h.in @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL #ifndef CHIAKI_CONFIG_H #define CHIAKI_CONFIG_H diff --git a/lib/include/chiaki/audio.h b/lib/include/chiaki/audio.h index d713181..3d454f8 100644 --- a/lib/include/chiaki/audio.h +++ b/lib/include/chiaki/audio.h @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL #ifndef CHIAKI_AUDIO_H #define CHIAKI_AUDIO_H diff --git a/lib/include/chiaki/audioreceiver.h b/lib/include/chiaki/audioreceiver.h index f499d09..228b27c 100644 --- a/lib/include/chiaki/audioreceiver.h +++ b/lib/include/chiaki/audioreceiver.h @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL #ifndef CHIAKI_AUDIORECEIVER_H #define CHIAKI_AUDIORECEIVER_H diff --git a/lib/include/chiaki/base64.h b/lib/include/chiaki/base64.h index 7f53ea5..3c44b4b 100644 --- a/lib/include/chiaki/base64.h +++ b/lib/include/chiaki/base64.h @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL #ifndef CHIAKI_BASE64_H #define CHIAKI_BASE64_H diff --git a/lib/include/chiaki/common.h b/lib/include/chiaki/common.h index 97b02c3..651d9fa 100644 --- a/lib/include/chiaki/common.h +++ b/lib/include/chiaki/common.h @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL #ifndef CHIAKI_COMMON_H #define CHIAKI_COMMON_H diff --git a/lib/include/chiaki/congestioncontrol.h b/lib/include/chiaki/congestioncontrol.h index c1e183e..3760399 100644 --- a/lib/include/chiaki/congestioncontrol.h +++ b/lib/include/chiaki/congestioncontrol.h @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL #ifndef CHIAKI_CONGESTIONCONTROL_H #define CHIAKI_CONGESTIONCONTROL_H diff --git a/lib/include/chiaki/controller.h b/lib/include/chiaki/controller.h index 193f402..e8f2e7c 100644 --- a/lib/include/chiaki/controller.h +++ b/lib/include/chiaki/controller.h @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL #ifndef CHIAKI_CONTROLLER_H #define CHIAKI_CONTROLLER_H diff --git a/lib/include/chiaki/ctrl.h b/lib/include/chiaki/ctrl.h index c219b3d..4152071 100644 --- a/lib/include/chiaki/ctrl.h +++ b/lib/include/chiaki/ctrl.h @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL #ifndef CHIAKI_CTRL_H #define CHIAKI_CTRL_H diff --git a/lib/include/chiaki/discovery.h b/lib/include/chiaki/discovery.h index 9459062..b9fa2ec 100644 --- a/lib/include/chiaki/discovery.h +++ b/lib/include/chiaki/discovery.h @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL #ifndef CHIAKI_DISCOVERY_H #define CHIAKI_DISCOVERY_H diff --git a/lib/include/chiaki/discoveryservice.h b/lib/include/chiaki/discoveryservice.h index 03dce7d..8cebda4 100644 --- a/lib/include/chiaki/discoveryservice.h +++ b/lib/include/chiaki/discoveryservice.h @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL #ifndef CHIAKI_DISCOVERYSERVICE_H diff --git a/lib/include/chiaki/ecdh.h b/lib/include/chiaki/ecdh.h index e55c03f..018457f 100644 --- a/lib/include/chiaki/ecdh.h +++ b/lib/include/chiaki/ecdh.h @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL #ifndef CHIAKI_ECDH_H #define CHIAKI_ECDH_H diff --git a/lib/include/chiaki/fec.h b/lib/include/chiaki/fec.h index c6b22db..2a4b789 100644 --- a/lib/include/chiaki/fec.h +++ b/lib/include/chiaki/fec.h @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL #ifndef CHIAKI_FEC_H #define CHIAKI_FEC_H diff --git a/lib/include/chiaki/feedback.h b/lib/include/chiaki/feedback.h index 11bc707..6573a83 100644 --- a/lib/include/chiaki/feedback.h +++ b/lib/include/chiaki/feedback.h @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL #ifndef CHIAKI_FEEDBACK_H #define CHIAKI_FEEDBACK_H diff --git a/lib/include/chiaki/feedbacksender.h b/lib/include/chiaki/feedbacksender.h index 63eff00..da202e8 100644 --- a/lib/include/chiaki/feedbacksender.h +++ b/lib/include/chiaki/feedbacksender.h @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL #ifndef CHIAKI_FEEDBACKSENDER_H #define CHIAKI_FEEDBACKSENDER_H diff --git a/lib/include/chiaki/frameprocessor.h b/lib/include/chiaki/frameprocessor.h index 78d26f0..bcac03e 100644 --- a/lib/include/chiaki/frameprocessor.h +++ b/lib/include/chiaki/frameprocessor.h @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL #ifndef CHIAKI_FRAMEPROCESSOR_H #define CHIAKI_FRAMEPROCESSOR_H diff --git a/lib/include/chiaki/gkcrypt.h b/lib/include/chiaki/gkcrypt.h index a75d35e..591b243 100644 --- a/lib/include/chiaki/gkcrypt.h +++ b/lib/include/chiaki/gkcrypt.h @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL #ifndef CHIAKI_GKCRYPT_H #define CHIAKI_GKCRYPT_H diff --git a/lib/include/chiaki/http.h b/lib/include/chiaki/http.h index 034eeb5..f0ac444 100644 --- a/lib/include/chiaki/http.h +++ b/lib/include/chiaki/http.h @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL #ifndef CHIAKI_HTTP_H #define CHIAKI_HTTP_H diff --git a/lib/include/chiaki/launchspec.h b/lib/include/chiaki/launchspec.h index 529c9af..0606280 100644 --- a/lib/include/chiaki/launchspec.h +++ b/lib/include/chiaki/launchspec.h @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL #ifndef CHIAKI_LAUNCHSPEC_H #define CHIAKI_LAUNCHSPEC_H diff --git a/lib/include/chiaki/log.h b/lib/include/chiaki/log.h index a6789ee..bfaeeba 100644 --- a/lib/include/chiaki/log.h +++ b/lib/include/chiaki/log.h @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL #ifndef CHIAKI_LOG_H #define CHIAKI_LOG_H diff --git a/lib/include/chiaki/opusdecoder.h b/lib/include/chiaki/opusdecoder.h index 2504f37..75ab8da 100644 --- a/lib/include/chiaki/opusdecoder.h +++ b/lib/include/chiaki/opusdecoder.h @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL #ifndef CHIAKI_OPUSDECODER_H #define CHIAKI_OPUSDECODER_H diff --git a/lib/include/chiaki/packetstats.h b/lib/include/chiaki/packetstats.h index dfe4cb9..104c8a9 100644 --- a/lib/include/chiaki/packetstats.h +++ b/lib/include/chiaki/packetstats.h @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL #ifndef CHIAKI_PACKETSTATS_H #define CHIAKI_PACKETSTATS_H diff --git a/lib/include/chiaki/random.h b/lib/include/chiaki/random.h index 7b757eb..9898ec5 100644 --- a/lib/include/chiaki/random.h +++ b/lib/include/chiaki/random.h @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL #ifndef CHIAKI_RANDOM_H #define CHIAKI_RANDOM_H diff --git a/lib/include/chiaki/regist.h b/lib/include/chiaki/regist.h index 65617b9..c12d57f 100644 --- a/lib/include/chiaki/regist.h +++ b/lib/include/chiaki/regist.h @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL #ifndef CHIAKI_REGIST_H #define CHIAKI_REGIST_H diff --git a/lib/include/chiaki/reorderqueue.h b/lib/include/chiaki/reorderqueue.h index 9cdbf72..e972d91 100644 --- a/lib/include/chiaki/reorderqueue.h +++ b/lib/include/chiaki/reorderqueue.h @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL #ifndef CHIAKI_REORDERQUEUE_H #define CHIAKI_REORDERQUEUE_H diff --git a/lib/include/chiaki/rpcrypt.h b/lib/include/chiaki/rpcrypt.h index 2f24632..4415db8 100644 --- a/lib/include/chiaki/rpcrypt.h +++ b/lib/include/chiaki/rpcrypt.h @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL #ifndef CHIAKI_RPCRYPT_H #define CHIAKI_RPCRYPT_H diff --git a/lib/include/chiaki/senkusha.h b/lib/include/chiaki/senkusha.h index f4912a7..4e74ed2 100644 --- a/lib/include/chiaki/senkusha.h +++ b/lib/include/chiaki/senkusha.h @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL #ifndef CHIAKI_SENKUSHA_H #define CHIAKI_SENKUSHA_H diff --git a/lib/include/chiaki/seqnum.h b/lib/include/chiaki/seqnum.h index 897603f..703a681 100644 --- a/lib/include/chiaki/seqnum.h +++ b/lib/include/chiaki/seqnum.h @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL #ifndef CHIAKI_SEQNUM_H #define CHIAKI_SEQNUM_H diff --git a/lib/include/chiaki/session.h b/lib/include/chiaki/session.h index 8b4ae97..72e9041 100644 --- a/lib/include/chiaki/session.h +++ b/lib/include/chiaki/session.h @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL #ifndef CHIAKI_SESSION_H #define CHIAKI_SESSION_H diff --git a/lib/include/chiaki/sock.h b/lib/include/chiaki/sock.h index e50ed01..9ddae9c 100644 --- a/lib/include/chiaki/sock.h +++ b/lib/include/chiaki/sock.h @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL #ifndef CHIAKI_SOCK_H #define CHIAKI_SOCK_H diff --git a/lib/include/chiaki/stoppipe.h b/lib/include/chiaki/stoppipe.h index 60715d1..3c585b2 100644 --- a/lib/include/chiaki/stoppipe.h +++ b/lib/include/chiaki/stoppipe.h @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL #ifndef CHIAKI_STOPPIPE_H #define CHIAKI_STOPPIPE_H diff --git a/lib/include/chiaki/streamconnection.h b/lib/include/chiaki/streamconnection.h index ed080f6..ba1817a 100644 --- a/lib/include/chiaki/streamconnection.h +++ b/lib/include/chiaki/streamconnection.h @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL #ifndef CHIAKI_STREAMCONNECTION_H #define CHIAKI_STREAMCONNECTION_H diff --git a/lib/include/chiaki/takion.h b/lib/include/chiaki/takion.h index f957948..01b5521 100644 --- a/lib/include/chiaki/takion.h +++ b/lib/include/chiaki/takion.h @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL #ifndef CHIAKI_TAKION_H #define CHIAKI_TAKION_H diff --git a/lib/include/chiaki/takionsendbuffer.h b/lib/include/chiaki/takionsendbuffer.h index 74a54b2..8ddecc0 100644 --- a/lib/include/chiaki/takionsendbuffer.h +++ b/lib/include/chiaki/takionsendbuffer.h @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL #ifndef CHIAKI_TAKIONSENDBUFFER_H #define CHIAKI_TAKIONSENDBUFFER_H diff --git a/lib/include/chiaki/thread.h b/lib/include/chiaki/thread.h index 0c441bf..621a1ce 100644 --- a/lib/include/chiaki/thread.h +++ b/lib/include/chiaki/thread.h @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL #ifndef CHIAKI_THREAD_H #define CHIAKI_THREAD_H diff --git a/lib/include/chiaki/time.h b/lib/include/chiaki/time.h index a1e6818..6e31d07 100644 --- a/lib/include/chiaki/time.h +++ b/lib/include/chiaki/time.h @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL #ifndef CHIAKI_TIME_H #define CHIAKI_TIME_H diff --git a/lib/include/chiaki/video.h b/lib/include/chiaki/video.h index 28ea851..3ff3ee8 100644 --- a/lib/include/chiaki/video.h +++ b/lib/include/chiaki/video.h @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL #ifndef CHIAKI_VIDEO_H #define CHIAKI_VIDEO_H diff --git a/lib/include/chiaki/videoreceiver.h b/lib/include/chiaki/videoreceiver.h index 111d64f..4a814c2 100644 --- a/lib/include/chiaki/videoreceiver.h +++ b/lib/include/chiaki/videoreceiver.h @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL #ifndef CHIAKI_VIDEORECEIVER_H #define CHIAKI_VIDEORECEIVER_H diff --git a/lib/src/audio.c b/lib/src/audio.c index 529e2df..c7f8dce 100644 --- a/lib/src/audio.c +++ b/lib/src/audio.c @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL #include diff --git a/lib/src/audioreceiver.c b/lib/src/audioreceiver.c index 88b4758..9aeaa61 100644 --- a/lib/src/audioreceiver.c +++ b/lib/src/audioreceiver.c @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL #include #include diff --git a/lib/src/base64.c b/lib/src/base64.c index 5882ff8..fbdf635 100644 --- a/lib/src/base64.c +++ b/lib/src/base64.c @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL #include diff --git a/lib/src/common.c b/lib/src/common.c index 5f00d07..21b8850 100644 --- a/lib/src/common.c +++ b/lib/src/common.c @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL #include #include diff --git a/lib/src/congestioncontrol.c b/lib/src/congestioncontrol.c index 227f5bd..e478410 100644 --- a/lib/src/congestioncontrol.c +++ b/lib/src/congestioncontrol.c @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL #include diff --git a/lib/src/controller.c b/lib/src/controller.c index c8d97b7..b347bb7 100644 --- a/lib/src/controller.c +++ b/lib/src/controller.c @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL #include diff --git a/lib/src/ctrl.c b/lib/src/ctrl.c index 1c83568..f953e05 100644 --- a/lib/src/ctrl.c +++ b/lib/src/ctrl.c @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL #include #include diff --git a/lib/src/discovery.c b/lib/src/discovery.c index 05c2c25..fb910a4 100644 --- a/lib/src/discovery.c +++ b/lib/src/discovery.c @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL #include "utils.h" diff --git a/lib/src/discoveryservice.c b/lib/src/discoveryservice.c index 58f9996..5ec84e6 100644 --- a/lib/src/discoveryservice.c +++ b/lib/src/discoveryservice.c @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL #include diff --git a/lib/src/ecdh.c b/lib/src/ecdh.c index 12a1da5..1a48b54 100644 --- a/lib/src/ecdh.c +++ b/lib/src/ecdh.c @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL #include #include diff --git a/lib/src/fec.c b/lib/src/fec.c index d3e0797..2441d4a 100644 --- a/lib/src/fec.c +++ b/lib/src/fec.c @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL #include diff --git a/lib/src/feedback.c b/lib/src/feedback.c index e53d192..05c29b9 100644 --- a/lib/src/feedback.c +++ b/lib/src/feedback.c @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL #include #include diff --git a/lib/src/feedbacksender.c b/lib/src/feedbacksender.c index 724dba4..dc67bf6 100644 --- a/lib/src/feedbacksender.c +++ b/lib/src/feedbacksender.c @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL #include diff --git a/lib/src/frameprocessor.c b/lib/src/frameprocessor.c index c5de224..058f6ac 100644 --- a/lib/src/frameprocessor.c +++ b/lib/src/frameprocessor.c @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL #include #include diff --git a/lib/src/gkcrypt.c b/lib/src/gkcrypt.c index 800a98e..c99c5c2 100644 --- a/lib/src/gkcrypt.c +++ b/lib/src/gkcrypt.c @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL #include #include diff --git a/lib/src/http.c b/lib/src/http.c index 662ddac..3bda9f1 100644 --- a/lib/src/http.c +++ b/lib/src/http.c @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL #include diff --git a/lib/src/launchspec.c b/lib/src/launchspec.c index 8134dd0..d3b71b0 100644 --- a/lib/src/launchspec.c +++ b/lib/src/launchspec.c @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL #include #include diff --git a/lib/src/log.c b/lib/src/log.c index 8678011..b62fc46 100644 --- a/lib/src/log.c +++ b/lib/src/log.c @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL #include diff --git a/lib/src/opusdecoder.c b/lib/src/opusdecoder.c index a171d22..31eaf9c 100644 --- a/lib/src/opusdecoder.c +++ b/lib/src/opusdecoder.c @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL #include #if CHIAKI_LIB_ENABLE_OPUS diff --git a/lib/src/packetstats.c b/lib/src/packetstats.c index dce9df1..c04adc8 100644 --- a/lib/src/packetstats.c +++ b/lib/src/packetstats.c @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL #include #include diff --git a/lib/src/pb_utils.h b/lib/src/pb_utils.h index 70ecd5a..d4b30be 100644 --- a/lib/src/pb_utils.h +++ b/lib/src/pb_utils.h @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL #ifndef CHIAKI_PB_UTILS_H #define CHIAKI_PB_UTILS_H diff --git a/lib/src/random.c b/lib/src/random.c index 47ac272..974db60 100644 --- a/lib/src/random.c +++ b/lib/src/random.c @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL #include diff --git a/lib/src/regist.c b/lib/src/regist.c index f56d721..1181b4d 100644 --- a/lib/src/regist.c +++ b/lib/src/regist.c @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL #include "utils.h" diff --git a/lib/src/reorderqueue.c b/lib/src/reorderqueue.c index cee6fef..bd007fe 100644 --- a/lib/src/reorderqueue.c +++ b/lib/src/reorderqueue.c @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL #include diff --git a/lib/src/rpcrypt.c b/lib/src/rpcrypt.c index 5b4bfba..bfab3e8 100644 --- a/lib/src/rpcrypt.c +++ b/lib/src/rpcrypt.c @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL #include diff --git a/lib/src/senkusha.c b/lib/src/senkusha.c index 69b8dc4..1fa33d4 100644 --- a/lib/src/senkusha.c +++ b/lib/src/senkusha.c @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL #include #include diff --git a/lib/src/session.c b/lib/src/session.c index a70bae7..50d3ece 100644 --- a/lib/src/session.c +++ b/lib/src/session.c @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL #include #include diff --git a/lib/src/sock.c b/lib/src/sock.c index 6f9a220..b126bc2 100644 --- a/lib/src/sock.c +++ b/lib/src/sock.c @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL #include #include diff --git a/lib/src/stoppipe.c b/lib/src/stoppipe.c index 9b699d9..dd069ef 100644 --- a/lib/src/stoppipe.c +++ b/lib/src/stoppipe.c @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL #include #include diff --git a/lib/src/streamconnection.c b/lib/src/streamconnection.c index f3fde0d..25caeb6 100644 --- a/lib/src/streamconnection.c +++ b/lib/src/streamconnection.c @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL #include diff --git a/lib/src/takion.c b/lib/src/takion.c index 80daf4f..3e4a463 100644 --- a/lib/src/takion.c +++ b/lib/src/takion.c @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL #include #include diff --git a/lib/src/takionsendbuffer.c b/lib/src/takionsendbuffer.c index cb878d9..030476b 100644 --- a/lib/src/takionsendbuffer.c +++ b/lib/src/takionsendbuffer.c @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL #ifndef CHIAKI_UNIT_TEST diff --git a/lib/src/thread.c b/lib/src/thread.c index 2cd11ee..1948311 100644 --- a/lib/src/thread.c +++ b/lib/src/thread.c @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL #define _GNU_SOURCE diff --git a/lib/src/time.c b/lib/src/time.c index c64787e..67ad7d8 100644 --- a/lib/src/time.c +++ b/lib/src/time.c @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL #include diff --git a/lib/src/utils.h b/lib/src/utils.h index ce28fb1..9f96d73 100644 --- a/lib/src/utils.h +++ b/lib/src/utils.h @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL #ifndef CHIAKI_UTILS_H #define CHIAKI_UTILS_H diff --git a/lib/src/videoreceiver.c b/lib/src/videoreceiver.c index 4238bd6..52beb02 100644 --- a/lib/src/videoreceiver.c +++ b/lib/src/videoreceiver.c @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL #include #include diff --git a/setsu/demo/main.c b/setsu/demo/main.c index 17b3f2b..4219638 100644 --- a/setsu/demo/main.c +++ b/setsu/demo/main.c @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL #include diff --git a/setsu/include/setsu.h b/setsu/include/setsu.h index 1231f7e..5e546bc 100644 --- a/setsu/include/setsu.h +++ b/setsu/include/setsu.h @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL #ifndef _SETSU_H #define _SETSU_H diff --git a/setsu/src/setsu.c b/setsu/src/setsu.c index fb87ba1..f899d07 100644 --- a/setsu/src/setsu.c +++ b/setsu/src/setsu.c @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL #include diff --git a/switch/include/discoverymanager.h b/switch/include/discoverymanager.h index 88574e8..d1222ab 100644 --- a/switch/include/discoverymanager.h +++ b/switch/include/discoverymanager.h @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL #ifndef CHIAKI_DISCOVERYMANAGER_H #define CHIAKI_DISCOVERYMANAGER_H diff --git a/switch/include/exception.h b/switch/include/exception.h index e101773..9a6492c 100644 --- a/switch/include/exception.h +++ b/switch/include/exception.h @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL #ifndef CHIAKI_EXCEPTION_H #define CHIAKI_EXCEPTION_H diff --git a/switch/include/gui.h b/switch/include/gui.h index 1226cd1..2f07e73 100644 --- a/switch/include/gui.h +++ b/switch/include/gui.h @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL #ifndef CHIAKI_GUI_H #define CHIAKI_GUI_H diff --git a/switch/include/host.h b/switch/include/host.h index 1d5bd9a..6be0b14 100644 --- a/switch/include/host.h +++ b/switch/include/host.h @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL #ifndef CHIAKI_HOST_H #define CHIAKI_HOST_H diff --git a/switch/include/io.h b/switch/include/io.h index 1fdd7ac..932a2b3 100644 --- a/switch/include/io.h +++ b/switch/include/io.h @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL #ifndef CHIAKI_IO_H #define CHIAKI_IO_H diff --git a/switch/include/settings.h b/switch/include/settings.h index 2ede438..1a8eb03 100644 --- a/switch/include/settings.h +++ b/switch/include/settings.h @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL #ifndef CHIAKI_SETTINGS_H #define CHIAKI_SETTINGS_H diff --git a/switch/src/discoverymanager.cpp b/switch/src/discoverymanager.cpp index cbd3ff0..4c0ebbe 100644 --- a/switch/src/discoverymanager.cpp +++ b/switch/src/discoverymanager.cpp @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL #ifdef __SWITCH__ #include diff --git a/switch/src/gui.cpp b/switch/src/gui.cpp index e5838d4..1f29410 100644 --- a/switch/src/gui.cpp +++ b/switch/src/gui.cpp @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL #include #include "gui.h" diff --git a/switch/src/host.cpp b/switch/src/host.cpp index 2315975..3779377 100644 --- a/switch/src/host.cpp +++ b/switch/src/host.cpp @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL #include diff --git a/switch/src/io.cpp b/switch/src/io.cpp index 3e15bd0..c81788e 100644 --- a/switch/src/io.cpp +++ b/switch/src/io.cpp @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL #ifdef __SWITCH__ #include diff --git a/switch/src/main.cpp b/switch/src/main.cpp index 1a48318..8b0a682 100644 --- a/switch/src/main.cpp +++ b/switch/src/main.cpp @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL // chiaki modules #include diff --git a/switch/src/settings.cpp b/switch/src/settings.cpp index 9e5a7ee..ed8d222 100644 --- a/switch/src/settings.cpp +++ b/switch/src/settings.cpp @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL #include #include diff --git a/test/fec.c b/test/fec.c index ebbedb0..2c7a634 100644 --- a/test/fec.c +++ b/test/fec.c @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL #include diff --git a/test/gkcrypt.c b/test/gkcrypt.c index 3071aab..5b83142 100644 --- a/test/gkcrypt.c +++ b/test/gkcrypt.c @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL #include diff --git a/test/http.c b/test/http.c index fd1d896..9cfc06c 100644 --- a/test/http.c +++ b/test/http.c @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL #include diff --git a/test/keystate.c b/test/keystate.c index 74ca77a..9629d25 100644 --- a/test/keystate.c +++ b/test/keystate.c @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL #include diff --git a/test/main.c b/test/main.c index 5618b69..c3a67f8 100644 --- a/test/main.c +++ b/test/main.c @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL #include diff --git a/test/regist.c b/test/regist.c index fc2ce93..00dcc00 100644 --- a/test/regist.c +++ b/test/regist.c @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL #include diff --git a/test/reorderqueue.c b/test/reorderqueue.c index a5e4fbf..e5a90c9 100644 --- a/test/reorderqueue.c +++ b/test/reorderqueue.c @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL #include diff --git a/test/rpcrypt.c b/test/rpcrypt.c index 0a25770..8f07950 100644 --- a/test/rpcrypt.c +++ b/test/rpcrypt.c @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL #include diff --git a/test/seqnum.c b/test/seqnum.c index 7bd09b7..6f2927e 100644 --- a/test/seqnum.c +++ b/test/seqnum.c @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL #include diff --git a/test/takion.c b/test/takion.c index 8cdf02e..fd2bc86 100644 --- a/test/takion.c +++ b/test/takion.c @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL #include diff --git a/test/test_log.c b/test/test_log.c index 55d995e..2d1e2e2 100644 --- a/test/test_log.c +++ b/test/test_log.c @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL #include "test_log.h" diff --git a/test/test_log.h b/test/test_log.h index 10a5023..9f8d6db 100644 --- a/test/test_log.h +++ b/test/test_log.h @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL #ifndef CHIAKI_TEST_LOG_H #define CHIAKI_TEST_LOG_H From db5b3a1499fdb4b7ec278c7830b7d81d823d68e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Sun, 27 Dec 2020 16:43:06 +0100 Subject: [PATCH 088/237] Add macOS to AppVeyor --- .appveyor.yml | 66 ++++++++++++++------ scripts/{appveyor.sh => appveyor-win.sh} | 0 scripts/{travis-build.sh => build-common.sh} | 3 +- 3 files changed, 48 insertions(+), 21 deletions(-) rename scripts/{appveyor.sh => appveyor-win.sh} (100%) rename scripts/{travis-build.sh => build-common.sh} (90%) diff --git a/.appveyor.yml b/.appveyor.yml index 44a5dbc..7d49f38 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -1,4 +1,6 @@ -image: 'Visual Studio 2017' +image: + - macOS + - 'Visual Studio 2019' branches: only: @@ -9,26 +11,50 @@ branches: configuration: - Release -install: - - git submodule update --init --recursive +for: + - matrix: + only: + - image: 'Visual Studio 2019' -build_script: - - call "C:\Program Files (x86)\Microsoft Visual Studio\2017\Community\VC\Auxiliary\Build\vcvarsall.bat" x64 - - C:\msys64\usr\bin\bash -lc "cd \"%APPVEYOR_BUILD_FOLDER%\" && scripts/appveyor.sh" + install: + - git submodule update --init --recursive -artifacts: - - path: Chiaki - name: Chiaki - - path: Chiaki-PDB - name: Chiaki-PDB + build_script: + - call "C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Auxiliary\Build\vcvarsall.bat" x64 + - C:\msys64\usr\bin\bash -lc "cd \"%APPVEYOR_BUILD_FOLDER%\" && scripts/appveyor-win.sh" -deploy: - description: 'Chiaki Binaries' - provider: GitHub - draft: true - auth_token: - secure: Amvzm3PMM5nv+iFsqaU7TZ9fgyYt/YIrOLV0QMiCyOoUlLRIaiYxWiJ7maTpxhZ9 - artifact: "Chiaki" - on: - appveyor_repo_tag: true + artifacts: + - path: Chiaki + name: Chiaki + - path: Chiaki-PDB + name: Chiaki-PDB + - matrix: + only: + - image: macOS + + install: + - git submodule update --init --recursive + - sudo pip3 install protobuf + - brew install qt opus openssl@1.1 nasm sdl2 protobuf + - scripts/build-ffmpeg.sh + + build_script: + - export CMAKE_PREFIX_PATH="`pwd`/ffmpeg-prefix;/usr/local/opt/openssl@1.1;/usr/local/opt/qt" + - scripts/build-common.sh + - cp -a build/gui/chiaki.app Chiaki.app + - /usr/local/opt/qt/bin/macdeployqt Chiaki.app -dmg + + artifacts: + - path: Chiaki.dmg + name: Chiaki + +#deploy: +# description: 'Chiaki Binaries' +# provider: GitHub +# draft: true +# auth_token: +# secure: Amvzm3PMM5nv+iFsqaU7TZ9fgyYt/YIrOLV0QMiCyOoUlLRIaiYxWiJ7maTpxhZ9 +# artifact: "Chiaki" +# on: +# appveyor_repo_tag: true diff --git a/scripts/appveyor.sh b/scripts/appveyor-win.sh similarity index 100% rename from scripts/appveyor.sh rename to scripts/appveyor-win.sh diff --git a/scripts/travis-build.sh b/scripts/build-common.sh similarity index 90% rename from scripts/travis-build.sh rename to scripts/build-common.sh index a16ec77..0eb87b9 100755 --- a/scripts/travis-build.sh +++ b/scripts/build-common.sh @@ -10,4 +10,5 @@ cmake \ $CMAKE_EXTRA_ARGS \ .. || exit 1 make -j4 || exit 1 -test/chiaki-unit || exit 1 \ No newline at end of file +test/chiaki-unit || exit 1 + From dfc0f32ac203fd99ee94e882017035262c9a28f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Sun, 27 Dec 2020 19:21:09 +0100 Subject: [PATCH 089/237] Move Rest of CI to Sourcehut --- .builds/alpine.yml | 26 ----- .builds/android.yml | 2 +- .builds/common.yml | 46 ++++++++ .github/workflows/bullseye.yml | 22 ---- .github/workflows/switch.yml | 24 ---- .gitignore | 1 + .travis.yml | 165 --------------------------- README.md | 12 +- lib/include/chiaki/config.h | 9 ++ scripts/Dockerfile.bullseye | 2 +- scripts/Dockerfile.xenial | 13 +++ scripts/appveyor-win.sh | 2 +- scripts/build-appimage.sh | 43 +++++++ scripts/build-ffmpeg.sh | 2 + scripts/build-sdl2.sh | 13 ++- scripts/ci-script | 9 -- scripts/fetch-protoc.sh | 7 +- scripts/run-docker-build-appimage.sh | 15 +++ scripts/run-docker-build-bullseye.sh | 14 +++ scripts/travis-appimage.sh | 16 --- 20 files changed, 165 insertions(+), 278 deletions(-) delete mode 100644 .builds/alpine.yml create mode 100644 .builds/common.yml delete mode 100644 .github/workflows/bullseye.yml delete mode 100644 .github/workflows/switch.yml delete mode 100644 .travis.yml create mode 100644 lib/include/chiaki/config.h create mode 100644 scripts/Dockerfile.xenial create mode 100755 scripts/build-appimage.sh delete mode 100755 scripts/ci-script create mode 100755 scripts/run-docker-build-appimage.sh create mode 100755 scripts/run-docker-build-bullseye.sh delete mode 100644 scripts/travis-appimage.sh diff --git a/.builds/alpine.yml b/.builds/alpine.yml deleted file mode 100644 index d6d394e..0000000 --- a/.builds/alpine.yml +++ /dev/null @@ -1,26 +0,0 @@ - -image: alpine/latest - -sources: - - https://github.com/thestr4ng3r/chiaki.git - -packages: - - cmake - - protoc - - py3-protobuf - - opus-dev - - qt5-qtbase-dev - - qt5-qtsvg-dev - - qt5-qtmultimedia-dev - - ffmpeg-dev - - sdl2-dev - - sdl2-static # this is gone on alpine edge so might be necessary to remove later - -tasks: - - build: | - cd chiaki - mkdir build && cd build - cmake .. - make -j4 - test/chiaki-unit - diff --git a/.builds/android.yml b/.builds/android.yml index 9398064..2f6361a 100644 --- a/.builds/android.yml +++ b/.builds/android.yml @@ -4,7 +4,7 @@ packages: - docker sources: -- https://github.com/thestr4ng3r/chiaki.git +- https://github.com/thestr4ng3r/chiaki artifacts: - Chiaki.apk diff --git a/.builds/common.yml b/.builds/common.yml new file mode 100644 index 0000000..d59cf54 --- /dev/null +++ b/.builds/common.yml @@ -0,0 +1,46 @@ + +image: alpine/latest + +sources: + - https://github.com/thestr4ng3r/chiaki + +packages: + - cmake + - ninja + - protoc + - py3-protobuf + - opus-dev + - qt5-qtbase-dev + - qt5-qtsvg-dev + - qt5-qtmultimedia-dev + - ffmpeg-dev + - sdl2-dev + - sdl2-static # this is gone on alpine edge so might be necessary to remove later + - docker + - fuse + +artifacts: + - chiaki.nro + - Chiaki.AppImage + +tasks: + - start_docker: | + sudo service docker start + sudo chmod +s /usr/bin/docker # Yes, I know what I am doing + sudo service fuse start # Fuse for AppImages + - local_build_and_test: | + cd chiaki + cmake -Bbuild -GNinja + ninja -C build + build/test/chiaki-unit + - appimage: | + cd chiaki + scripts/run-docker-build-appimage.sh + cp appimage/Chiaki.AppImage ../Chiaki.AppImage + - switch: | + cd chiaki + scripts/switch/run-docker-build-chiaki.sh + cp build_switch/switch/chiaki.nro ../chiaki.nro + - bullseye: | + cd chiaki + scripts/run-docker-build-bullseye.sh diff --git a/.github/workflows/bullseye.yml b/.github/workflows/bullseye.yml deleted file mode 100644 index 5afd2c7..0000000 --- a/.github/workflows/bullseye.yml +++ /dev/null @@ -1,22 +0,0 @@ -name: Debian Bullseye -on: - push: - branches: - - master - pull_request: - -jobs: - build: - name: Debian Bullseye - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - name: Checkout submodules - run: | - git submodule init - git submodule update - - name: Docker Build - run: cd scripts && docker build -t chiaki-bullseye . -f Dockerfile.bullseye && cd .. - - name: Build and Test - run: docker run --rm -v "`pwd`:/build" -t chiaki-bullseye /bin/bash -c "cd /build && scripts/ci-script -DCHIAKI_USE_SYSTEM_JERASURE=ON -DCHIAKI_USE_SYSTEM_NANOPB=ON" - diff --git a/.github/workflows/switch.yml b/.github/workflows/switch.yml deleted file mode 100644 index b365ab6..0000000 --- a/.github/workflows/switch.yml +++ /dev/null @@ -1,24 +0,0 @@ -name: Switch -on: - push: - branches: - - master - pull_request: - -jobs: - build: - name: Switch - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - name: Checkout submodules - run: | - git submodule init - git submodule update - - name: Build Chiaki - run: scripts/switch/run-docker-build-chiaki.sh - - name: Upload chiaki.nro - uses: actions/upload-artifact@v1 - with: - name: chiaki.nro - path: ./build_switch/switch/chiaki.nro diff --git a/.gitignore b/.gitignore index 8cbe89f..9977c53 100644 --- a/.gitignore +++ b/.gitignore @@ -27,3 +27,4 @@ compile_commands.json .cache/ .gdb_history chiaki.conf +/appimage diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 7af6368..0000000 --- a/.travis.yml +++ /dev/null @@ -1,165 +0,0 @@ - -language: cpp - -branches: - only: - - master - - /^v\d.*$/ - - /^deploy-test(-.*)?$/ - -before_script: - - export CHIAKI_VERSION="$TRAVIS_TAG" - - if [ -z "$CHIAKI_VERSION" ]; then export CHIAKI_VERSION="$TRAVIS_COMMIT"; fi - -matrix: - include: - - name: Linux (Bionic) - os: linux - dist: bionic - addons: - apt: - sources: - - sourceline: "ppa:beineri/opt-qt-5.12.0-bionic" - packages: - - protobuf-compiler - - python3-protobuf - - libopus-dev - - qt512base - - qt512multimedia - - qt512gamepad - - qt512svg - - libgl1-mesa-dev - - nasm - - libsdl2-dev - - libva-dev - env: - - CMAKE_PREFIX_PATH="$TRAVIS_BUILD_DIR/ffmpeg-prefix;/opt/qt512" - - CMAKE_EXTRA_ARGS="-DCMAKE_INSTALL_PREFIX=/usr" - - DEPLOY=0 - install: - - scripts/build-ffmpeg.sh - script: - - scripts/travis-build.sh || exit 1 - - source scripts/travis-appimage.sh - - - name: Linux (Xenial, Deploy) - os: linux - dist: xenial - addons: - apt: - sources: - - sourceline: "ppa:beineri/opt-qt-5.12.3-xenial" - packages: - - python3-pip - - libopus-dev - - qt512base - - qt512multimedia - - qt512svg - - libgl1-mesa-dev - - nasm - - libudev-dev - - libva-dev - env: - - CMAKE_PREFIX_PATH="$TRAVIS_BUILD_DIR/ffmpeg-prefix;$TRAVIS_BUILD_DIR/sdl2-prefix;/opt/qt512" - - CMAKE_EXTRA_ARGS="-DCMAKE_INSTALL_PREFIX=/usr" - - SDL2_FROM_SRC=1 - - DEPLOY=1 - install: - - sudo pip3 install protobuf - - scripts/fetch-protoc.sh - - export PATH="$TRAVIS_BUILD_DIR/protoc/bin:$PATH" - - scripts/build-ffmpeg.sh - - scripts/build-sdl2.sh - script: - - scripts/travis-build.sh || exit 1 - - source scripts/travis-appimage.sh - - - name: macOS - os: osx - osx_image: xcode11 - addons: - homebrew: - packages: - - qt - - opus - - openssl@1.1 - - nasm - - sdl2 - env: - - CMAKE_PREFIX_PATH="$TRAVIS_BUILD_DIR/ffmpeg-prefix;/usr/local/opt/openssl@1.1;/usr/local/opt/qt" - - CMAKE_EXTRA_ARGS="" - - DEPLOY=1 - install: - - pip3 install protobuf - - scripts/build-ffmpeg.sh - script: - - scripts/travis-build.sh - - cp -a build/gui/chiaki.app Chiaki.app - - /usr/local/opt/qt/bin/macdeployqt Chiaki.app -dmg - - export DEPLOY_FILE="Chiaki-${CHIAKI_VERSION}-macOS-x86_64.dmg" - - mv Chiaki.dmg "$DEPLOY_FILE" - - cmake -DCHIAKI_VERSION="${CHIAKI_VERSION}" -DCHIAKI_DMG="${DEPLOY_FILE}" -DCHIAKI_CASK_OUT=chiaki.rb -P scripts/configure-cask.cmake - - echo "------------------- chiaki.rb cask -------------------" - - cat chiaki.rb - - echo "------------------------------------------------------" - - - name: Android - language: android - os: linux - dist: trusty - env: - - DEPLOY=1 - android: - components: - - build-tools-29.0.2 - - android-29 - addons: - apt: - packages: - - python3-pip - install: - - echo y | sdkmanager "ndk-bundle" - - echo y | sdkmanager "cmake;3.10.2.4988404" - - sudo pip3 install protobuf - - scripts/fetch-protoc.sh - - export PATH="$TRAVIS_BUILD_DIR/protoc/bin:$PATH" - script: - - cd android - - ./gradlew assembleRelease bundleRelease - - if [ "$TRAVIS_PULL_REQUEST" = "false" ]; then export DEPLOY_FILE_BASE="Chiaki-$CHIAKI_VERSION-Android"; fi - - if [ "$TRAVIS_PULL_REQUEST" = "false" ]; then export DEPLOY_FILE="$DEPLOY_FILE_BASE.a[pa][kb]"; fi - - if [ "$TRAVIS_PULL_REQUEST" = "false" ]; then cp app/build/outputs/apk/release/app-release.apk "../$DEPLOY_FILE_BASE.apk"; fi - - if [ "$TRAVIS_PULL_REQUEST" = "false" ]; then cp app/build/outputs/bundle/release/app-release.aab "../$DEPLOY_FILE_BASE.aab"; fi - - cd .. - - - name: "Source Package" - os: linux - dist: bionic - env: - - DEPLOY=1 - install: ~ - script: - - find . -name ".git*" | xargs rm -rfv - - mkdir chiaki && shopt -s extglob && mv !(chiaki) chiaki - - export DEPLOY_FILE="chiaki-$CHIAKI_VERSION-src.tar.gz" - - tar -czvf "$DEPLOY_FILE" chiaki - -before_install: - - if [ ! -z "$encrypted_31d5e6477a29_iv" ]; then openssl aes-256-cbc -K $encrypted_31d5e6477a29_key -iv $encrypted_31d5e6477a29_iv -in secret.tar.enc -out secret.tar -d && tar -xf secret.tar; fi - -after_success: - - if [ ! -z "$DEPLOY_FILE" ]; then echo "--- SHA256 for $DEPLOY_FILE"; openssl dgst -sha256 "$DEPLOY_FILE"; echo; fi - - if [ ! -z "$DEPLOY_FILE" ]; then echo "--- transfer.sh"; curl -m 30 --upload-file "$DEPLOY_FILE" "https://transfer.sh/$DEPLOY_FILE"; echo; fi - - if [ ! -z "$DEPLOY_FILE" ]; then echo "--- oshi.at"; curl -m 30 --upload-file "$DEPLOY_FILE" "https://oshi.at/$DEPLOY_FILE/129600"; echo; fi - -deploy: - skip_cleanup: true - provider: releases - draft: true - api_key: - secure: R7RjLOuGFda05EJeNX2lNG135xKU2w9IQn7p1H1P2zw4zlQMgSBpNRaW8hE408x5KJUjptJTF6QaYYmPWbHlf9VEPFVIcVzSp8YSd2Cdr+GKhmFgWF+fJPBj5y9NNqohwxvK3Nrugh0v6yVQiEYEGF7WArU6dvymSNNTw/EqXtfrOvwUgSf1bDAzQAsXn3E6Ptzf9DrQU8+mOgMSqT/3Wy5207KLmWTtwBWDgkskKwS9OEXk3tDd6U4uT7NFHHmcw+ZjQXRD+yHSHUWYs1oKR4IfgPFxQfEK0Txhkxdf3yj1aNweuk7GGC3cfRaarUfRQpoYqYYCxhTfGZ2b4rVgX3XpssMY7ZmSZHRi/SX08ETXF/c7PZGzr0RPFXZLgAGjgN6O2Dbb9agc3tOUGDUuqKEWX9sALm82WS0FRAFrFLENgMFsj5hu+DKIIkAU2yEsadYKjjhC+q+mTAEkxKKknvM50Xpx3tE1TlP/31Z53v4/NydHIHXPJ72V3mnuoTacwhG2SkGtjMbLCnEZDCtu9C4556oa7Z29cqafv90ZD7lTQMV+ijKvjxgOC9u1GeemmZLofRGDFyYSqKxOpYxxxXGOhs+7FMAdKP00h++MTLwRwIebKQs0fW0XiNKmwushWOUU8sXI1jxTbwe9dPQsspxHRv/mVo6l2vUcBjC19K0= - file_glob: true - file: $DEPLOY_FILE - on: - tags: true - condition: $DEPLOY = 1 diff --git a/README.md b/README.md index bbcbd75..7eca7d0 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ **Disclaimer:** This project is not endorsed or certified by Sony Interactive Entertainment LLC. -[![Travis Build Status](https://travis-ci.com/thestr4ng3r/chiaki.svg?branch=master)](https://travis-ci.com/thestr4ng3r/chiaki) [![AppVeyor Build status](https://ci.appveyor.com/api/projects/status/dnj9cmx5mmaaawer?svg=true)](https://ci.appveyor.com/project/thestr4ng3r/chiaki) [![builds.sr.ht Status](https://builds.sr.ht/~thestr4ng3r/chiaki.svg)](https://builds.sr.ht/~thestr4ng3r/chiaki?) +[![AppVeyor Build status](https://ci.appveyor.com/api/projects/status/dnj9cmx5mmaaawer?svg=true)](https://ci.appveyor.com/project/thestr4ng3r/chiaki) [![builds.sr.ht Status](https://builds.sr.ht/~thestr4ng3r/chiaki.svg)](https://builds.sr.ht/~thestr4ng3r/chiaki?) Chiaki is a Free and Open Source Software Client for PlayStation 4 Remote Play for Linux, FreeBSD, OpenBSD, Android, macOS, Windows, Nintendo Switch and potentially even more platforms. @@ -50,7 +50,7 @@ make ``` For more detailed platform-specific instructions, see [doc/platform-build.md](doc/platform-build.md). - +in ## Usage If your PS4 is on your local network, is turned on or in standby mode and does not have Discovery explicitly disabled, Chiaki should find it. @@ -83,10 +83,10 @@ There are official groups for Chiaki on Telegram and IRC. They are bridged so yo ## Acknowledgements This project has only been made possible because of the following Open Source projects: -[radare2](https://github.com/radare/radare2), -[Cutter](https://cutter.re/), -[Frida](https://www.frida.re/) and -[x64dbg](https://x64dbg.com/). +[Rizin](https://rizin.re), +[Cutter](https://cutter.re), +[Frida](https://www.frida.re) and +[x64dbg](https://x64dbg.com). Also thanks to [delroth](https://github.com/delroth) for analyzing the registration and wakeup protocol, [grill2010](https://github.com/grill2010) for analyzing the PSN's OAuth Login, diff --git a/lib/include/chiaki/config.h b/lib/include/chiaki/config.h new file mode 100644 index 0000000..542ff64 --- /dev/null +++ b/lib/include/chiaki/config.h @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL + +#ifndef CHIAKI_CONFIG_H +#define CHIAKI_CONFIG_H + +#define CHIAKI_LIB_ENABLE_OPUS 1 +#define CHIAKI_LIB_ENABLE_PI_DECODER 0 + +#endif // CHIAKI_CONFIG_H diff --git a/scripts/Dockerfile.bullseye b/scripts/Dockerfile.bullseye index 96cab09..888f3af 100644 --- a/scripts/Dockerfile.bullseye +++ b/scripts/Dockerfile.bullseye @@ -2,7 +2,7 @@ FROM debian:bullseye RUN apt-get update -RUN apt-get -y install git g++ cmake pkg-config \ +RUN apt-get -y install git g++ cmake ninja-build pkg-config \ libjerasure-dev nanopb libnanopb-dev libavcodec-dev libopus-dev \ libssl-dev protobuf-compiler python3 python3-protobuf \ libevdev-dev libudev-dev \ diff --git a/scripts/Dockerfile.xenial b/scripts/Dockerfile.xenial new file mode 100644 index 0000000..b741028 --- /dev/null +++ b/scripts/Dockerfile.xenial @@ -0,0 +1,13 @@ + +FROM ubuntu:xenial + +RUN apt-get update +RUN apt-get install -y software-properties-common +RUN add-apt-repository ppa:beineri/opt-qt-5.12.3-xenial +RUN apt-get update +RUN apt-get -y install git g++ cmake ninja-build curl unzip python3-pip \ + libssl-dev libopus-dev qt512base qt512multimedia qt512svg \ + libgl1-mesa-dev nasm libudev-dev libva-dev fuse + +CMD [] + diff --git a/scripts/appveyor-win.sh b/scripts/appveyor-win.sh index 0e49286..f751200 100755 --- a/scripts/appveyor-win.sh +++ b/scripts/appveyor-win.sh @@ -12,7 +12,7 @@ cd .. || exit 1 export PATH="$PWD/ninja:$PWD/yasm:/c/Qt/5.12/msvc2017_64/bin:$PATH" -scripts/build-ffmpeg.sh --target-os=win64 --arch=x86_64 --toolchain=msvc || exit 1 +scripts/build-ffmpeg.sh . --target-os=win64 --arch=x86_64 --toolchain=msvc || exit 1 git clone https://github.com/xiph/opus.git && cd opus && git checkout ad8fe90db79b7d2a135e3dfd2ed6631b0c5662ab || exit 1 mkdir build && cd build || exit 1 diff --git a/scripts/build-appimage.sh b/scripts/build-appimage.sh new file mode 100755 index 0000000..7ba4acf --- /dev/null +++ b/scripts/build-appimage.sh @@ -0,0 +1,43 @@ +#!/bin/bash + +set -xe + +mkdir appimage + +pip3 install --user protobuf +scripts/fetch-protoc.sh appimage +export PATH="`pwd`/appimage/protoc/bin:$PATH" +scripts/build-ffmpeg.sh appimage +scripts/build-sdl2.sh appimage + +mkdir build_appimage +cd build_appimage +cmake \ + -GNinja \ + -DCMAKE_BUILD_TYPE=Release \ + "-DCMAKE_PREFIX_PATH=`pwd`/../appimage/ffmpeg-prefix;`pwd`/../appimage/sdl2-prefix;/opt/qt512" \ + -DCHIAKI_ENABLE_TESTS=ON \ + -DCHIAKI_ENABLE_CLI=OFF \ + -DCHIAKI_GUI_ENABLE_SDL_GAMECONTROLLER=ON \ + -DCMAKE_INSTALL_PREFIX=/usr \ + .. +cd .. +ninja -C build_appimage +build_appimage/test/chiaki-unit + +DESTDIR=`pwd`/appimage/appdir ninja -C build_appimage install +cd appimage + +curl -L -O https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage +chmod +x linuxdeploy-x86_64.AppImage +curl -L -O https://github.com/linuxdeploy/linuxdeploy-plugin-qt/releases/download/continuous/linuxdeploy-plugin-qt-x86_64.AppImage +chmod +x linuxdeploy-plugin-qt-x86_64.AppImage +set +e +source /opt/qt512/bin/qt512-env.sh +set -e + +export LD_LIBRARY_PATH="`pwd`/sdl2-prefix/lib:$LD_LIBRARY_PATH" +export EXTRA_QT_PLUGINS=opengl + +./linuxdeploy-x86_64.AppImage --appdir=appdir -e appdir/usr/bin/chiaki -d appdir/usr/share/applications/chiaki.desktop --plugin qt --output appimage +mv Chiaki-*-x86_64.AppImage Chiaki.AppImage diff --git a/scripts/build-ffmpeg.sh b/scripts/build-ffmpeg.sh index dfce4bb..00289fd 100755 --- a/scripts/build-ffmpeg.sh +++ b/scripts/build-ffmpeg.sh @@ -1,6 +1,8 @@ #!/bin/bash cd $(dirname "${BASH_SOURCE[0]}")/.. +cd "./$1" +shift ROOT="`pwd`" TAG=n4.2 diff --git a/scripts/build-sdl2.sh b/scripts/build-sdl2.sh index 7382bca..f06b9cc 100755 --- a/scripts/build-sdl2.sh +++ b/scripts/build-sdl2.sh @@ -1,6 +1,9 @@ #!/bin/bash +set -xe + cd $(dirname "${BASH_SOURCE[0]}")/.. +cd "./$1" ROOT="`pwd`" URL=https://www.libsdl.org/release/SDL2-2.0.10.tar.gz @@ -8,8 +11,8 @@ FILE=SDL2-2.0.10.tar.gz DIR=SDL2-2.0.10 if [ ! -d "$DIR" ]; then - wget "$URL" || exit 1 - tar -xf "$FILE" || exit 1 + curl -L "$URL" -O + tar -xf "$FILE" fi cd "$DIR" || exit 1 @@ -33,9 +36,9 @@ cmake \ -DSDL_THREADS=ON \ -DSDL_TIMERS=OFF \ -DSDL_VIDEO=OFF \ - .. || exit 1 + .. # SDL_THREADS is not needed, but it doesn't compile without -make -j4 || exit 1 -make install || exit 1 +make -j4 +make install diff --git a/scripts/ci-script b/scripts/ci-script deleted file mode 100755 index e88acfb..0000000 --- a/scripts/ci-script +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/bash - -set -e -set -x - -mkdir build && cd build -cmake .. -DCHIAKI_ENABLE_SETSU=ON "$@" -make -j8 -make test diff --git a/scripts/fetch-protoc.sh b/scripts/fetch-protoc.sh index 2dec889..e1d2d2f 100755 --- a/scripts/fetch-protoc.sh +++ b/scripts/fetch-protoc.sh @@ -1,10 +1,13 @@ #!/bin/bash +set -xe + cd $(dirname "${BASH_SOURCE[0]}")/.. +cd "./$1" ROOT="`pwd`" URL=https://github.com/protocolbuffers/protobuf/releases/download/v3.9.1/protoc-3.9.1-linux-x86_64.zip -wget "$URL" -O protoc.zip || exit 1 -unzip protoc.zip -d protoc || exit 1 +curl -L "$URL" -o protoc.zip +unzip protoc.zip -d protoc diff --git a/scripts/run-docker-build-appimage.sh b/scripts/run-docker-build-appimage.sh new file mode 100755 index 0000000..931499c --- /dev/null +++ b/scripts/run-docker-build-appimage.sh @@ -0,0 +1,15 @@ +#!/bin/bash + +set -xe +cd "`dirname $(readlink -f ${0})`" + +docker build -t chiaki-xenial . -f Dockerfile.xenial +cd .. +docker run --rm \ + -v "`pwd`:/build/chiaki" \ + -w "/build/chiaki" \ + --device /dev/fuse \ + --cap-add SYS_ADMIN \ + -t chiaki-xenial \ + /bin/bash -c "scripts/build-appimage.sh" + diff --git a/scripts/run-docker-build-bullseye.sh b/scripts/run-docker-build-bullseye.sh new file mode 100755 index 0000000..fd19bb1 --- /dev/null +++ b/scripts/run-docker-build-bullseye.sh @@ -0,0 +1,14 @@ +#!/bin/bash + +set -xe +cd "`dirname $(readlink -f ${0})`" + +docker build -t chiaki-bullseye . -f Dockerfile.bullseye +cd .. +docker run --rm -v "`pwd`:/build" chiaki-bullseye /bin/bash -c " + cd /build && + mkdir build_bullseye && + cmake -Bbuild_bullseye -GNinja -DCHIAKI_ENABLE_SETSU=ON -DCHIAKI_USE_SYSTEM_JERASURE=ON -DCHIAKI_USE_SYSTEM_NANOPB=ON && + ninja -C build_bullseye && + ninja -C build_bullseye test" + diff --git a/scripts/travis-appimage.sh b/scripts/travis-appimage.sh deleted file mode 100644 index 6950e78..0000000 --- a/scripts/travis-appimage.sh +++ /dev/null @@ -1,16 +0,0 @@ -#!/bin/bash - -cd build && make install DESTDIR=../appdir && cd .. || exit 1 -wget https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage && chmod +x linuxdeploy-x86_64.AppImage || exit 1 -wget https://github.com/linuxdeploy/linuxdeploy-plugin-qt/releases/download/continuous/linuxdeploy-plugin-qt-x86_64.AppImage && chmod +x linuxdeploy-plugin-qt-x86_64.AppImage || exit 1 -source /opt/qt512/bin/qt512-env.sh || exit 1 - -if [ -n "$SDL2_FROM_SRC" ]; then - export LD_LIBRARY_PATH="$TRAVIS_BUILD_DIR/sdl2-prefix/lib:$LD_LIBRARY_PATH" || exit 1 -fi - -export EXTRA_QT_PLUGINS=opengl - -./linuxdeploy-x86_64.AppImage --appdir=appdir -e appdir/usr/bin/chiaki -d appdir/usr/share/applications/chiaki.desktop --plugin qt --output appimage || exit 1 -export DEPLOY_FILE="Chiaki-${CHIAKI_VERSION}-Linux-x86_64.AppImage" || exit 1 -mv Chiaki-*-x86_64.AppImage "$DEPLOY_FILE" || exit 1 \ No newline at end of file From b7c7d87db6defbabcaa9712cc44c331578bdf4dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Mon, 28 Dec 2020 12:34:03 +0100 Subject: [PATCH 090/237] Transition to Sourcehut --- .appveyor.yml | 10 ---- .builds/android.yml | 2 +- .builds/common.yml | 2 +- .builds/freebsd.yml | 2 +- .builds/openbsd.yml | 2 +- .github/ISSUE_TEMPLATE/bug_report.md | 46 ------------------- .github/ISSUE_TEMPLATE/feature_request.md | 31 ------------- .gitmodules | 4 +- README.md | 6 +-- android/app/src/main/res/values/strings.xml | 2 +- gui/re.chiaki.Chiaki.appdata.xml | 6 +-- scripts/Dockerfile | 46 ------------------- scripts/chiaki.rb.in | 16 ------- scripts/configure-cask.cmake | 11 ----- .../com.github.thestr4ng3r.Chiaki.json | 2 +- switch/README.md | 2 +- switch/src/io.cpp | 2 +- 17 files changed, 15 insertions(+), 177 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE/bug_report.md delete mode 100644 .github/ISSUE_TEMPLATE/feature_request.md delete mode 100644 scripts/Dockerfile delete mode 100644 scripts/chiaki.rb.in delete mode 100644 scripts/configure-cask.cmake diff --git a/.appveyor.yml b/.appveyor.yml index 7d49f38..933f517 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -48,13 +48,3 @@ for: artifacts: - path: Chiaki.dmg name: Chiaki - -#deploy: -# description: 'Chiaki Binaries' -# provider: GitHub -# draft: true -# auth_token: -# secure: Amvzm3PMM5nv+iFsqaU7TZ9fgyYt/YIrOLV0QMiCyOoUlLRIaiYxWiJ7maTpxhZ9 -# artifact: "Chiaki" -# on: -# appveyor_repo_tag: true diff --git a/.builds/android.yml b/.builds/android.yml index 2f6361a..7b64ab4 100644 --- a/.builds/android.yml +++ b/.builds/android.yml @@ -4,7 +4,7 @@ packages: - docker sources: -- https://github.com/thestr4ng3r/chiaki +- https://git.sr.ht/~thestr4ng3r/chiaki artifacts: - Chiaki.apk diff --git a/.builds/common.yml b/.builds/common.yml index d59cf54..46b754f 100644 --- a/.builds/common.yml +++ b/.builds/common.yml @@ -2,7 +2,7 @@ image: alpine/latest sources: - - https://github.com/thestr4ng3r/chiaki + - https://git.sr.ht/~thestr4ng3r/chiaki packages: - cmake diff --git a/.builds/freebsd.yml b/.builds/freebsd.yml index 404f51b..caa9016 100644 --- a/.builds/freebsd.yml +++ b/.builds/freebsd.yml @@ -2,7 +2,7 @@ image: freebsd/latest sources: - - https://github.com/thestr4ng3r/chiaki + - https://git.sr.ht/~thestr4ng3r/chiaki packages: - cmake diff --git a/.builds/openbsd.yml b/.builds/openbsd.yml index b7f5156..b8785ef 100644 --- a/.builds/openbsd.yml +++ b/.builds/openbsd.yml @@ -2,7 +2,7 @@ image: openbsd/6.7 sources: - - https://github.com/thestr4ng3r/chiaki + - https://git.sr.ht/~thestr4ng3r/chiaki packages: - cmake diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index 3168563..0000000 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,46 +0,0 @@ ---- -name: Bug report -about: Create a report to help us improve -title: '' -labels: '' -assignees: '' - ---- - - - -**Environment** - - OS/Distribution: - - Desktop Environment: - - Hardware: - - Exact Version or Commit Hash and Installation Method: - -**Describe the bug** -A clear and concise description of what the bug is. - -**To Reproduce** -Steps to reproduce the behavior: -1. Go to '...' -2. Click on '....' -3. Scroll down to '....' -4. See error - -**Expected behavior** -A clear and concise description of what you expected to happen. - -**Log Files** -If your issue is about Registration or Streaming, you MUST attach a log file of a session that showed your issue here. -Do not enable Verbose Logging unless explicitly told to. -On desktop, you can see the directory that log files are written to in the Settings under "Log Directory". -On Android, go into Settings -> Session Logs and export one from there. - -**Screenshots** -If applicable, add screenshots to help explain your problem. - -**Additional context** -Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md deleted file mode 100644 index c874dee..0000000 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -name: Feature request -about: Suggest an idea for this project -title: '' -labels: '' -assignees: '' - ---- - - - -**What platform does your feature request apply to?** -Choose one or more: Desktop/Android/Both/... - -**Is your feature request related to a problem? Please describe.** -A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] - -**Describe the solution you'd like** -A clear and concise description of what you want to happen. - -**Describe alternatives you've considered** -A clear and concise description of any alternative solutions or features you've considered. - -**Additional context** -Add any other context or screenshots about the feature request here. - diff --git a/.gitmodules b/.gitmodules index 6635f42..ac87125 100644 --- a/.gitmodules +++ b/.gitmodules @@ -6,10 +6,10 @@ url = https://github.com/nanopb/nanopb.git [submodule "third-party/jerasure"] path = third-party/jerasure - url = https://github.com/thestr4ng3r/jerasure.git + url = https://git.sr.ht/~thestr4ng3r/jerasure [submodule "third-party/gf-complete"] path = third-party/gf-complete - url = https://github.com/thestr4ng3r/gf-complete.git + url = https://git.sr.ht/~thestr4ng3r/gf-complete [submodule "android/app/src/main/cpp/oboe"] path = android/app/src/main/cpp/oboe url = https://github.com/google/oboe diff --git a/README.md b/README.md index 7eca7d0..ef5a797 100644 --- a/README.md +++ b/README.md @@ -17,8 +17,6 @@ for Linux, FreeBSD, OpenBSD, Android, macOS, Windows, Nintendo Switch and potent Everything necessary for a full streaming session, including the initial registration and wakeup of the console, is supported. The following features however are yet to be implemented: -* H264 Error Concealment (FEC and active error recovery however are implemented) -* Touchpad support (Triggering the Touchpad Button is currently possible from the keyboard though) * Rumble * Accelerometer/Gyroscope @@ -30,10 +28,10 @@ You can either download a pre-built release (easier) or build Chiaki from source Builds are provided for Linux, Android, macOS and Windows. -You can download them [here](https://github.com/thestr4ng3r/chiaki/releases). +You can download them [here](https://git.sr.ht/~thestr4ng3r/chiaki/refs). * **Linux**: The provided file is an [AppImage](https://appimage.org/). Simply make it executable (`chmod +x .AppImage`) and run it. -* **Android**: Install from [Google Play](https://play.google.com/store/apps/details?id=com.metallic.chiaki), [F-Droid](https://f-droid.org/packages/com.metallic.chiaki/) or download the APK from GitHub. +* **Android**: Install from [Google Play](https://play.google.com/store/apps/details?id=com.metallic.chiaki), [F-Droid](https://f-droid.org/packages/com.metallic.chiaki/) or download the APK from Sourcehut. * **macOS**: Drag the application from the `.dmg` into your Applications folder. * **Windows**: Extract the `.zip` file and execute `chiaki.exe`. * **Switch**: Follow README specific [instructions](./switch/README.md) diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml index 41ee3de..6b34497 100644 --- a/android/app/src/main/res/values/strings.xml +++ b/android/app/src/main/res/values/strings.xml @@ -31,7 +31,7 @@ PS4 ≥ 7.0, < 8 PS4 ≥ 8.0 About obtaining your Account ID, see - https://github.com/thestr4ng3r/chiaki/blob/master/README.md#obtaining-your-psn-accountid + https://git.sr.ht/~thestr4ng3r/chiaki/tree/master/item/README.md#obtaining-your-psn-accountid PSN Online ID (username, case-sensitive) PSN Account ID (8 bytes, base64) PIN diff --git a/gui/re.chiaki.Chiaki.appdata.xml b/gui/re.chiaki.Chiaki.appdata.xml index 36f3c86..ba494cc 100644 --- a/gui/re.chiaki.Chiaki.appdata.xml +++ b/gui/re.chiaki.Chiaki.appdata.xml @@ -8,8 +8,8 @@ CC0-1.0 AGPL-3.0-only Florian Märkl - https://github.com/thestr4ng3r/chiaki - https://github.com/thestr4ng3r/chiaki/blob/master/README.md#usage + https://chiaki.re + https://git.sr.ht/~thestr4ng3r/chiaki/tree/master/item/README.md#usage

Chiaki is a Free and Open Source Client for PlayStation 4 Remote Play. It can be used to play in real time on a PlayStation 4 as long as there is a network connection. @@ -17,7 +17,7 @@ - https://raw.githubusercontent.com/thestr4ng3r/chiaki/master/assets/screenshot.png + https://git.sr.ht/~thestr4ng3r/chiaki/blob/refs/heads/master/assets/screenshot.png chiaki@metallic.software diff --git a/scripts/Dockerfile b/scripts/Dockerfile deleted file mode 100644 index 87bfbe9..0000000 --- a/scripts/Dockerfile +++ /dev/null @@ -1,46 +0,0 @@ - -FROM utensils/opengl:19.0.8 AS builder -LABEL maintainer="chiaki-docker@florianmaerkl.de" -WORKDIR /app -RUN echo "@testing http://dl-cdn.alpinelinux.org/alpine/edge/testing" >> /etc/apk/repositories \ - && apk update \ - && apk --no-cache add \ - build-base \ - cmake \ - ffmpeg-dev \ - git \ - openssl \ - opus-dev \ - protobuf \ -# py3-protobuf@testing \ # Enable this when the OpenGL image updates alpine - qt5-qtbase \ - qt5-qtmultimedia-dev \ - qt5-qtsvg-dev \ - sdl2-dev \ - && pip3 install protobuf \ - && git clone https://github.com/thestr4ng3r/chiaki.git /app \ - && git submodule update --init \ - && mkdir build \ - && cd build \ - && cmake .. \ - && make \ - && ./test/chiaki-unit \ - && rm -rf /var/cache/apk/* - -FROM utensils/opengl:19.0.8 -RUN apk update \ - && apk --no-cache add \ - ffmpeg \ - qt5-qtbase \ - qt5-qtmultimedia \ - qt5-qtsvg \ - sdl2 \ - && rm -rf /var/cache/apk/* \ - && addgroup ps4 && adduser -D -G ps4 ps4 \ - && chown -R ps4: /home/ps4 -WORKDIR /home/ps4 -COPY --from=builder /app/build/gui/chiaki . -USER ps4 -VOLUME /home/ps4/.local/share/Chiaki -CMD ["/home/ps4/chiaki"] - diff --git a/scripts/chiaki.rb.in b/scripts/chiaki.rb.in deleted file mode 100644 index c9fd136..0000000 --- a/scripts/chiaki.rb.in +++ /dev/null @@ -1,16 +0,0 @@ -cask 'chiaki' do - version '@CHIAKI_VERSION@' - sha256 '@CHIAKI_DMG_SHA256@' - - url "https://github.com/thestr4ng3r/chiaki/releases/download/v#{version}/Chiaki-v#{version}-macOS-x86_64.dmg" - appcast 'https://github.com/thestr4ng3r/chiaki/releases.atom' - name 'Chiaki' - homepage 'https://github.com/thestr4ng3r/chiaki' - - app 'Chiaki.app' - - zap trash: [ - '~/Library/Application Support/Chiaki', - '~/Library/Preferences/com.chiaki.Chiaki.plist', - ] -end diff --git a/scripts/configure-cask.cmake b/scripts/configure-cask.cmake deleted file mode 100644 index 62a7dae..0000000 --- a/scripts/configure-cask.cmake +++ /dev/null @@ -1,11 +0,0 @@ - -if(NOT CHIAKI_VERSION OR NOT CHIAKI_DMG OR NOT CHIAKI_CASK_OUT) - message(FATAL_ERROR "CHIAKI_VERSION, CHIAKI_DMG and CHIAKI_CASK_OUT must be set.") -endif() - -if(CHIAKI_VERSION MATCHES "^v([0-9].*)$") - set(CHIAKI_VERSION "${CMAKE_MATCH_1}") -endif() - -file(SHA256 "${CHIAKI_DMG}" CHIAKI_DMG_SHA256) -configure_file("${CMAKE_CURRENT_LIST_DIR}/chiaki.rb.in" "${CHIAKI_CASK_OUT}") diff --git a/scripts/flatpak/com.github.thestr4ng3r.Chiaki.json b/scripts/flatpak/com.github.thestr4ng3r.Chiaki.json index 905ac2a..7cc6c43 100644 --- a/scripts/flatpak/com.github.thestr4ng3r.Chiaki.json +++ b/scripts/flatpak/com.github.thestr4ng3r.Chiaki.json @@ -88,7 +88,7 @@ "sources": [ { "type": "git", - "url": "https://github.com/thestr4ng3r/chiaki.git", + "url": "https://git.sr.ht/~thestr4ng3r/chiaki", "tag": "v1.3.0", "commit": "702d31eb01d37518e77f5c1be3ea493df9f18323" } diff --git a/switch/README.md b/switch/README.md index e536d9b..9a5f6db 100644 --- a/switch/README.md +++ b/switch/README.md @@ -55,7 +55,7 @@ this file contains sensitive data. (do not share this file) host_ip = X.X.X.X # required: sony oline id (login) psn_online_id = ps4_online_id -# required (PS4>7.0 Only): https://github.com/thestr4ng3r/chiaki#obtaining-your-psn-accountid +# required (PS4>7.0 Only): https://git.sr.ht/~thestr4ng3r/chiaki/tree/master/item/README.md#obtaining-your-psn-accountid psn_account_id = ps4_base64_account_id # optional(default 60): remote play fps (must be 30 or 60) video_fps = 60 diff --git a/switch/src/io.cpp b/switch/src/io.cpp index c81788e..bcbfd76 100644 --- a/switch/src/io.cpp +++ b/switch/src/io.cpp @@ -15,7 +15,7 @@ #define SWITCH_TOUCHSCREEN_MAX_Y 720 // source: -// https://github.com/thestr4ng3r/chiaki/blob/master/gui/src/avopenglwidget.cpp +// gui/src/avopenglwidget.cpp // // examples : // https://www.roxlu.com/2014/039/decoding-h264-and-yuv420p-playback From c804bf66c70c4baee0734f066492003fc60d6471 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Mon, 28 Dec 2020 15:31:32 +0100 Subject: [PATCH 091/237] Remove last old CI remnants --- README.md | 2 +- android/app/build.gradle | 10 +--------- secret.tar.enc | Bin 10256 -> 0 bytes 3 files changed, 2 insertions(+), 10 deletions(-) delete mode 100644 secret.tar.enc diff --git a/README.md b/README.md index ef5a797..6648e75 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ **Disclaimer:** This project is not endorsed or certified by Sony Interactive Entertainment LLC. -[![AppVeyor Build status](https://ci.appveyor.com/api/projects/status/dnj9cmx5mmaaawer?svg=true)](https://ci.appveyor.com/project/thestr4ng3r/chiaki) [![builds.sr.ht Status](https://builds.sr.ht/~thestr4ng3r/chiaki.svg)](https://builds.sr.ht/~thestr4ng3r/chiaki?) +[![AppVeyor Build status](https://ci.appveyor.com/api/projects/status/c81ogebvsmo43dd3?svg=true)](https://ci.appveyor.com/project/thestr4ng3r/chiaki) [![builds.sr.ht Status](https://builds.sr.ht/~thestr4ng3r/chiaki.svg)](https://builds.sr.ht/~thestr4ng3r/chiaki?) Chiaki is a Free and Open Source Software Client for PlayStation 4 Remote Play for Linux, FreeBSD, OpenBSD, Android, macOS, Windows, Nintendo Switch and potentially even more platforms. diff --git a/android/app/build.gradle b/android/app/build.gradle index 585b3c1..8e32ae3 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -67,15 +67,7 @@ android { properties.load(propertiesFile.newDataInputStream()) } - def keystoreFile = file("../../keystore.jks") - if(System.getenv("TRAVIS") == "true" && keystoreFile.exists()) { - println("Enabling Signing on Travis") - buildTypes.release.signingConfig = signingConfigs.release - signingConfigs.release.storeFile = keystoreFile - signingConfigs.release.storePassword = System.getenv("android_keystore_pw") - signingConfigs.release.keyAlias = "chiaki" - signingConfigs.release.keyPassword = System.getenv("android_keystore_alias_pw") - } else if(properties.containsKey("chiakiKeystore")) { + if(properties.containsKey("chiakiKeystore")) { println("Enabling Local Signing") buildTypes.release.signingConfig = signingConfigs.release buildTypes.debug.signingConfig = signingConfigs.release diff --git a/secret.tar.enc b/secret.tar.enc deleted file mode 100644 index eff91182418b0be2b55507a365f45cef47be33df..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10256 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

4Ell6xkACiQG~l+}sAlwg<`t z!8s{t=qMI)crM{noKb=w-GYIv_+)mEJ+>OU^RU#lbF6mlZWEeyA7or>M?WRe!Vl#pi8N2c z^wHlR@;Trkeejor1p>zrT`yPXC%Cx;&W0Yn>U=qqPc+84Y8H0R&Vy!yT+)wBtOAMV zS|F&+5uh>o{ zxLO2hB3FH!T^q|oa0+K)QkP2`9}(46+YMGBlYSj}dH;rGFH;a7LW5dp6qk~tAJJ!f zVMh1xeETUm+*i7xY3F)xLR$TX)S^t_3=wZ6{NM#6@r!3R?79*q$Q?@?Nbde-FPQGd z;8?8ef(lD<^xEo;i;qcKP=35W*cD(=g8%%sd>Jd`8r=0<0v;#z_bgA)w&y?B zgA# Date: Fri, 11 Dec 2020 16:47:39 +0100 Subject: [PATCH 092/237] Prepare Regist for PS5 --- gui/include/registdialog.h | 1 + gui/src/registdialog.cpp | 25 ++++++++++++++--------- lib/include/chiaki/common.h | 17 +++++++++++++--- lib/include/chiaki/session.h | 4 +++- lib/src/regist.c | 39 ++++++++++++++++++++++++++++-------- lib/src/rpcrypt.c | 7 +++++-- lib/src/session.c | 25 +++++++++++++++-------- 7 files changed, 86 insertions(+), 32 deletions(-) diff --git a/gui/include/registdialog.h b/gui/include/registdialog.h index 06098ee..7d16b0f 100644 --- a/gui/include/registdialog.h +++ b/gui/include/registdialog.h @@ -29,6 +29,7 @@ class RegistDialog : public QDialog QRadioButton *ps4_pre9_radio_button; QRadioButton *ps4_pre10_radio_button; QRadioButton *ps4_10_radio_button; + QRadioButton *ps5_radio_button; QLineEdit *psn_online_id_edit; QLineEdit *psn_account_id_edit; QLineEdit *pin_edit; diff --git a/gui/src/registdialog.cpp b/gui/src/registdialog.cpp index 50c0cad..c101bf3 100644 --- a/gui/src/registdialog.cpp +++ b/gui/src/registdialog.cpp @@ -49,24 +49,27 @@ RegistDialog::RegistDialog(Settings *settings, const QString &host, QWidget *par psn_account_id_edit->setEnabled(need_account_id); }; - auto version_layout = new QVBoxLayout(nullptr); - ps4_pre9_radio_button = new QRadioButton(tr("< 7.0"), this); - version_layout->addWidget(ps4_pre9_radio_button); + auto target_layout = new QVBoxLayout(nullptr); + ps4_pre9_radio_button = new QRadioButton(tr("PS4 Firmware < 7.0"), this); + target_layout->addWidget(ps4_pre9_radio_button); connect(ps4_pre9_radio_button, &QRadioButton::toggled, this, UpdatePSNIDEdits); - ps4_pre10_radio_button = new QRadioButton(tr(">= 7.0, < 8.0"), this); - version_layout->addWidget(ps4_pre10_radio_button); + ps4_pre10_radio_button = new QRadioButton(tr("PS4 Firmware >= 7.0, < 8.0"), this); + target_layout->addWidget(ps4_pre10_radio_button); connect(ps4_pre10_radio_button, &QRadioButton::toggled, this, UpdatePSNIDEdits); - ps4_10_radio_button = new QRadioButton(tr(">= 8.0"), this); - version_layout->addWidget(ps4_10_radio_button); + ps4_10_radio_button = new QRadioButton(tr("PS4 Firmware >= 8.0"), this); + target_layout->addWidget(ps4_10_radio_button); connect(ps4_10_radio_button, &QRadioButton::toggled, this, UpdatePSNIDEdits); - form_layout->addRow(tr("PS4 Firmware:"), version_layout); + ps5_radio_button = new QRadioButton(tr("PS5"), this); + target_layout->addWidget(ps5_radio_button); + connect(ps5_radio_button, &QRadioButton::toggled, this, UpdatePSNIDEdits); + form_layout->addRow(tr("Console:"), target_layout); psn_online_id_edit = new QLineEdit(this); form_layout->addRow(tr("PSN Online-ID (username, case-sensitive):"), psn_online_id_edit); psn_account_id_edit = new QLineEdit(this); form_layout->addRow(tr("PSN Account-ID (base64):"), psn_account_id_edit); - ps4_10_radio_button->setChecked(true); + ps5_radio_button->setChecked(true); UpdatePSNIDEdits(); @@ -115,8 +118,10 @@ void RegistDialog::accept() info.target = CHIAKI_TARGET_PS4_8; else if(ps4_pre10_radio_button->isChecked()) info.target = CHIAKI_TARGET_PS4_9; - else + else if(ps4_10_radio_button->isChecked()) info.target = CHIAKI_TARGET_PS4_10; + else + info.target = CHIAKI_TARGET_PS5_1; bool need_account_id = NeedAccountId(); QByteArray psn_id; // keep this out of the if scope diff --git a/lib/include/chiaki/common.h b/lib/include/chiaki/common.h index 651d9fa..aa70cbc 100644 --- a/lib/include/chiaki/common.h +++ b/lib/include/chiaki/common.h @@ -11,6 +11,7 @@ #include #include +#include #ifdef __cplusplus extern "C" { @@ -61,11 +62,21 @@ typedef enum { // values must not change CHIAKI_TARGET_PS4_UNKNOWN = 0, - CHIAKI_TARGET_PS4_8 = 800, - CHIAKI_TARGET_PS4_9 = 900, - CHIAKI_TARGET_PS4_10 = 1000 + CHIAKI_TARGET_PS4_8 = 800, + CHIAKI_TARGET_PS4_9 = 900, + CHIAKI_TARGET_PS4_10 = 1000, + CHIAKI_TARGET_PS5_UNKNOWN = 1000000, + CHIAKI_TARGET_PS5_1 = 1000100 } ChiakiTarget; +static inline bool chiaki_target_is_unknown(ChiakiTarget target) +{ + return target == CHIAKI_TARGET_PS5_UNKNOWN + || target == CHIAKI_TARGET_PS4_UNKNOWN; +} + +static inline bool chiaki_target_is_ps5(ChiakiTarget target) { return target >= CHIAKI_TARGET_PS5_UNKNOWN; } + /** * Perform initialization of global state needed for using the Chiaki lib */ diff --git a/lib/include/chiaki/session.h b/lib/include/chiaki/session.h index 72e9041..5da7087 100644 --- a/lib/include/chiaki/session.h +++ b/lib/include/chiaki/session.h @@ -35,7 +35,7 @@ CHIAKI_EXPORT const char *chiaki_rp_application_reason_string(uint32_t reason); */ CHIAKI_EXPORT const char *chiaki_rp_version_string(ChiakiTarget target); -CHIAKI_EXPORT ChiakiTarget chiaki_rp_version_parse(const char *rp_version_str); +CHIAKI_EXPORT ChiakiTarget chiaki_rp_version_parse(const char *rp_version_str, bool is_ps5); #define CHIAKI_RP_DID_SIZE 32 @@ -70,6 +70,7 @@ CHIAKI_EXPORT void chiaki_connect_video_profile_preset(ChiakiConnectVideoProfile typedef struct chiaki_connect_info_t { + bool ps5; const char *host; // null terminated char regist_key[CHIAKI_SESSION_AUTH_SIZE]; // must be completely filled (pad with \0) uint8_t morning[0x10]; @@ -149,6 +150,7 @@ typedef struct chiaki_session_t { struct { + bool ps5; struct addrinfo *host_addrinfos; struct addrinfo *host_addrinfo_selected; char hostname[128]; diff --git a/lib/src/regist.c b/lib/src/regist.c index 1181b4d..07063bc 100644 --- a/lib/src/regist.c +++ b/lib/src/regist.c @@ -42,17 +42,21 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_regist_start(ChiakiRegist *regist, ChiakiLo if(!regist->info.host) return CHIAKI_ERR_MEMORY; + ChiakiErrorCode err = CHIAKI_ERR_UNKNOWN; if(regist->info.psn_online_id) { regist->info.psn_online_id = strdup(regist->info.psn_online_id); if(!regist->info.psn_online_id) + { + err = CHIAKI_ERR_MEMORY; goto error_host; + } } regist->cb = cb; regist->cb_user = cb_user; - ChiakiErrorCode err = chiaki_stop_pipe_init(®ist->stop_pipe); + err = chiaki_stop_pipe_init(®ist->stop_pipe); if(err != CHIAKI_ERR_SUCCESS) goto error_psn_id; @@ -98,9 +102,25 @@ static const char *const request_head_fmt = "Connection: close\r\n" "Content-Length: %llu\r\n"; -static const char *request_path = "/sie/ps4/rp/sess/rgst"; +static const char *request_path_ps5 = "/sie/ps5/rp/sess/rgst"; +static const char *request_path_ps4 = "/sie/ps4/rp/sess/rgst"; static const char *request_path_ps4_pre10 = "/sce/rp/regist"; +static const char *request_path(ChiakiTarget target) +{ + switch(target) + { + case CHIAKI_TARGET_PS5_UNKNOWN: + case CHIAKI_TARGET_PS5_1: + return request_path_ps5; + case CHIAKI_TARGET_PS4_8: + case CHIAKI_TARGET_PS4_9: + return request_path_ps4_pre10; + default: + return request_path_ps4; + } +} + static const char *const request_rp_version_fmt = "RP-Version: %s\r\n"; static const char *const request_tail = "\r\n"; @@ -119,8 +139,7 @@ static const char *const request_inner_online_id_fmt = static int request_header_format(char *buf, size_t buf_size, size_t payload_size, ChiakiTarget target) { - int cur = snprintf(buf, buf_size, request_head_fmt, - target < CHIAKI_TARGET_PS4_10 ? request_path_ps4_pre10 : request_path, + int cur = snprintf(buf, buf_size, request_head_fmt, request_path(target), (unsigned long long)payload_size); if(cur < 0 || cur >= payload_size) return -1; @@ -339,12 +358,16 @@ static ChiakiErrorCode regist_search(ChiakiRegist *regist, struct addrinfo *addr ChiakiErrorCode err = CHIAKI_ERR_SUCCESS; + const char *src = chiaki_target_is_ps5(regist->info.target) ? "SRC3" : "SRC2"; + const char *res = chiaki_target_is_ps5(regist->info.target) ? "RES3" : "RES2"; + size_t res_size = strlen(res); + CHIAKI_LOGI(regist->log, "Regist sending search packet"); int r; if(regist->info.broadcast) - r = sendto_broadcast(regist->log, sock, "SRC2", 4, 0, &send_addr, send_addr_len); + r = sendto_broadcast(regist->log, sock, src, strlen(src) + 1, 0, &send_addr, send_addr_len); else - r = send(sock, "SRC2", 4, 0); + r = send(sock, src, strlen(src) + 1, 0); if(r < 0) { CHIAKI_LOGE(regist->log, "Regist failed to send search: %s", strerror(errno)); @@ -379,10 +402,10 @@ static ChiakiErrorCode regist_search(ChiakiRegist *regist, struct addrinfo *addr goto done; } - CHIAKI_LOGV(regist->log, "Regist received packet:"); + CHIAKI_LOGV(regist->log, "Regist received packet: %d >= %d", n, res_size); chiaki_log_hexdump(regist->log, CHIAKI_LOG_VERBOSE, buf, n); - if(n >= 4 && memcmp(buf, "RES2", 4) == 0) + if(n >= res_size && !memcmp(buf, res, res_size)) { char addr[64]; const char *addr_str = sockaddr_str(recv_addr, addr, sizeof(addr)); diff --git a/lib/src/rpcrypt.c b/lib/src/rpcrypt.c index bfab3e8..270048b 100644 --- a/lib/src/rpcrypt.c +++ b/lib/src/rpcrypt.c @@ -971,18 +971,21 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_rpcrypt_init_regist(ChiakiRPCrypt *rpcrypt, } #define HMAC_KEY_SIZE 0x10 -static const uint8_t hmac_key[HMAC_KEY_SIZE] = { 0x20, 0xd6, 0x6f, 0x59, 0x04, 0xea, 0x7c, 0x14, 0xe5, 0x57, 0xff, 0xc5, 0x2e, 0x48, 0x8a, 0xc8 }; +static const uint8_t hmac_key_ps5[HMAC_KEY_SIZE] = { 0x46, 0x46, 0x87, 0xb3, 0x49, 0xca, 0x8c, 0xe8, 0x59, 0xc5, 0x27, 0x0f, 0x5d, 0x7a, 0x69, 0xd6 }; +static const uint8_t hmac_key_ps4[HMAC_KEY_SIZE] = { 0x20, 0xd6, 0x6f, 0x59, 0x04, 0xea, 0x7c, 0x14, 0xe5, 0x57, 0xff, 0xc5, 0x2e, 0x48, 0x8a, 0xc8 }; static const uint8_t hmac_key_ps4_pre10[HMAC_KEY_SIZE] = { 0xac, 0x07, 0x88, 0x83, 0xc8, 0x3a, 0x1f, 0xe8, 0x11, 0x46, 0x3a, 0xf3, 0x9e, 0xe3, 0xe3, 0x77 }; static const uint8_t *rpcrypt_hmac_key(ChiakiRPCrypt *rpcrypt) { switch(rpcrypt->target) { + case CHIAKI_TARGET_PS5_1: + return hmac_key_ps5; case CHIAKI_TARGET_PS4_8: case CHIAKI_TARGET_PS4_9: return hmac_key_ps4_pre10; default: - return hmac_key; + return hmac_key_ps4; } } diff --git a/lib/src/session.c b/lib/src/session.c index 50d3ece..ab33d97 100644 --- a/lib/src/session.c +++ b/lib/src/session.c @@ -62,18 +62,26 @@ const char *chiaki_rp_version_string(ChiakiTarget version) return "9.0"; case CHIAKI_TARGET_PS4_10: return "10.0"; + case CHIAKI_TARGET_PS5_1: + return "1.0"; default: return NULL; } } -CHIAKI_EXPORT ChiakiTarget chiaki_rp_version_parse(const char *rp_version_str) +CHIAKI_EXPORT ChiakiTarget chiaki_rp_version_parse(const char *rp_version_str, bool is_ps5) { - if(strcmp(rp_version_str, "8.0") == 0) + if(is_ps5) + { + if(!strcmp(rp_version_str, "1.0")) + return CHIAKI_TARGET_PS5_1; + return CHIAKI_TARGET_PS5_UNKNOWN; + } + if(!strcmp(rp_version_str, "8.0")) return CHIAKI_TARGET_PS4_8; - if(strcmp(rp_version_str, "9.0") == 0) + if(!strcmp(rp_version_str, "9.0")) return CHIAKI_TARGET_PS4_9; - if(strcmp(rp_version_str, "10.0") == 0) + if(!strcmp(rp_version_str, "10.0")) return CHIAKI_TARGET_PS4_10; return CHIAKI_TARGET_PS4_UNKNOWN; } @@ -205,6 +213,7 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_session_init(ChiakiSession *session, Chiaki chiaki_controller_state_set_idle(&session->controller_state); + session->connect_info.ps5 = connect_info->ps5; memcpy(session->connect_info.regist_key, connect_info->regist_key, sizeof(session->connect_info.regist_key)); memcpy(session->connect_info.morning, connect_info->morning, sizeof(session->connect_info.morning)); @@ -359,17 +368,17 @@ static void *session_thread_func(void *arg) CHIAKI_LOGI(session->log, "Starting session request"); - ChiakiTarget server_target = CHIAKI_TARGET_PS4_UNKNOWN; + ChiakiTarget server_target = session->connect_info.ps5 ? CHIAKI_TARGET_PS5_UNKNOWN : CHIAKI_TARGET_PS4_UNKNOWN; success = session_thread_request_session(session, &server_target); - if(!success && server_target != CHIAKI_TARGET_PS4_UNKNOWN) + if(!success && chiaki_target_is_unknown(server_target)) { CHIAKI_LOGI(session->log, "Attempting to re-request session with Server's RP-Version"); session->target = server_target; success = session_thread_request_session(session, &server_target); } - if(!success && server_target != CHIAKI_TARGET_PS4_UNKNOWN) + if(!success && chiaki_target_is_unknown(server_target)) { CHIAKI_LOGI(session->log, "Attempting to re-request session even harder with Server's RP-Version!!!"); session->target = server_target; @@ -757,7 +766,7 @@ static bool session_thread_request_session(ChiakiSession *session, ChiakiTarget { CHIAKI_LOGI(session->log, "Reported RP-Version mismatch. ours = %s, server = %s", rp_version_str ? rp_version_str : "", response.rp_version); - *target_out = chiaki_rp_version_parse(response.rp_version); + *target_out = chiaki_rp_version_parse(response.rp_version, session->connect_info.ps5); if(*target_out != CHIAKI_TARGET_PS4_UNKNOWN) CHIAKI_LOGI(session->log, "Detected Server RP-Version %s", chiaki_rp_version_string(*target_out)); else if(!strcmp(response.rp_version, "5.0")) From 20a7e9d12335faf82209f8010ebb4a073ea844fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Fri, 11 Dec 2020 18:35:49 +0100 Subject: [PATCH 093/237] Finish RpCrypt for PS5 Regist --- lib/include/chiaki/rpcrypt.h | 4 +- lib/src/regist.c | 8 +- lib/src/rpcrypt.c | 238 ++++++++++++++++++++++++++--------- test/rpcrypt.c | 67 ++++++++-- 4 files changed, 246 insertions(+), 71 deletions(-) diff --git a/lib/include/chiaki/rpcrypt.h b/lib/include/chiaki/rpcrypt.h index 4415db8..7df16a5 100644 --- a/lib/include/chiaki/rpcrypt.h +++ b/lib/include/chiaki/rpcrypt.h @@ -23,11 +23,11 @@ typedef struct chiaki_rpcrypt_t CHIAKI_EXPORT void chiaki_rpcrypt_bright_ambassador(ChiakiTarget target, uint8_t *bright, uint8_t *ambassador, const uint8_t *nonce, const uint8_t *morning); CHIAKI_EXPORT void chiaki_rpcrypt_aeropause_ps4_pre10(uint8_t *aeropause, const uint8_t *ambassador); -CHIAKI_EXPORT void chiaki_rpcrypt_aeropause(size_t key_1_off, uint8_t *aeropause, const uint8_t *ambassador); +CHIAKI_EXPORT ChiakiErrorCode chiaki_rpcrypt_aeropause(ChiakiTarget target, size_t key_1_off, uint8_t *aeropause, const uint8_t *ambassador); CHIAKI_EXPORT void chiaki_rpcrypt_init_auth(ChiakiRPCrypt *rpcrypt, ChiakiTarget target, const uint8_t *nonce, const uint8_t *morning); CHIAKI_EXPORT void chiaki_rpcrypt_init_regist_ps4_pre10(ChiakiRPCrypt *rpcrypt, const uint8_t *ambassador, uint32_t pin); -CHIAKI_EXPORT ChiakiErrorCode chiaki_rpcrypt_init_regist(ChiakiRPCrypt *rpcrypt, const uint8_t *ambassador, size_t key_0_off, uint32_t pin); +CHIAKI_EXPORT ChiakiErrorCode chiaki_rpcrypt_init_regist(ChiakiRPCrypt *rpcrypt, ChiakiTarget target, const uint8_t *ambassador, size_t key_0_off, uint32_t pin); CHIAKI_EXPORT ChiakiErrorCode chiaki_rpcrypt_generate_iv(ChiakiRPCrypt *rpcrypt, uint8_t *iv, uint64_t counter); CHIAKI_EXPORT ChiakiErrorCode chiaki_rpcrypt_encrypt(ChiakiRPCrypt *rpcrypt, uint64_t counter, const uint8_t *in, uint8_t *out, size_t sz); CHIAKI_EXPORT ChiakiErrorCode chiaki_rpcrypt_decrypt(ChiakiRPCrypt *rpcrypt, uint64_t counter, const uint8_t *in, uint8_t *out, size_t sz); diff --git a/lib/src/regist.c b/lib/src/regist.c index 07063bc..b2cee08 100644 --- a/lib/src/regist.c +++ b/lib/src/regist.c @@ -178,9 +178,13 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_regist_request_payload_format(ChiakiTarget { size_t key_0_off = buf[0x18D] & 0x1F; size_t key_1_off = buf[0] >> 3; - chiaki_rpcrypt_init_regist(crypt, ambassador, key_0_off, pin); + ChiakiErrorCode err = chiaki_rpcrypt_init_regist(crypt, target, ambassador, key_0_off, pin); + if(err != CHIAKI_ERR_SUCCESS) + return err; uint8_t aeropause[0x10]; - chiaki_rpcrypt_aeropause(key_1_off, aeropause, crypt->ambassador); + err = chiaki_rpcrypt_aeropause(target, key_1_off, aeropause, crypt->ambassador); + if(err != CHIAKI_ERR_SUCCESS) + return err; memcpy(buf + 0xc7, aeropause + 8, 8); memcpy(buf + 0x191, aeropause, 8); psn_online_id = NULL; // don't need this diff --git a/lib/src/rpcrypt.c b/lib/src/rpcrypt.c index 270048b..781ecb8 100644 --- a/lib/src/rpcrypt.c +++ b/lib/src/rpcrypt.c @@ -813,9 +813,9 @@ CHIAKI_EXPORT void chiaki_rpcrypt_aeropause_ps4_pre10(uint8_t *aeropause, const } } -CHIAKI_EXPORT void chiaki_rpcrypt_aeropause(size_t key_1_off, uint8_t *aeropause, const uint8_t *ambassador) +CHIAKI_EXPORT ChiakiErrorCode chiaki_rpcrypt_aeropause(ChiakiTarget target, size_t key_1_off, uint8_t *aeropause, const uint8_t *ambassador) { - const uint8_t keys_1[] = { + static const uint8_t ps4_keys_1[] = { 0xc8, 0x48, 0xc2, 0xb4, 0x08, 0xeb, 0x88, 0xf7, 0x5f, 0x4a, 0x09, 0x2d, 0x59, 0x1f, 0x09, 0xcd, 0x1c, 0x18, 0xf4, 0x7a, 0x28, 0x4a, 0x96, 0x6d, 0xb3, 0x59, 0x71, 0x53, 0x75, 0x7e, @@ -870,11 +870,73 @@ CHIAKI_EXPORT void chiaki_rpcrypt_aeropause(size_t key_1_off, uint8_t *aeropause 0x63, 0x27 }; + static const uint8_t ps5_keys_1[512] = { + 0x79, 0x4d, 0x78, 0x30, 0xfe, 0x10, 0x52, 0x4c, 0xa8, 0x90, + 0x5b, 0x9a, 0x7e, 0x5f, 0xd3, 0xe1, 0x13, 0xe0, 0xf1, 0x0f, + 0xa3, 0xe7, 0xbb, 0x45, 0x7f, 0xdc, 0x8e, 0xd5, 0xf1, 0x04, + 0x5c, 0x78, 0x51, 0xef, 0xf8, 0x65, 0x59, 0x03, 0x39, 0x84, + 0x37, 0xae, 0x59, 0xdf, 0x23, 0xb6, 0x60, 0x34, 0xe6, 0x4b, + 0xe2, 0xf5, 0x4c, 0x13, 0xc6, 0xda, 0xf9, 0xfd, 0xb3, 0x65, + 0x84, 0xd6, 0x45, 0xec, 0x2c, 0x00, 0xf2, 0xed, 0xdc, 0xcb, + 0x93, 0x6e, 0x61, 0x46, 0xe5, 0xd6, 0x01, 0x94, 0xee, 0x78, + 0x85, 0x0e, 0x68, 0x5e, 0xb5, 0x5b, 0xcd, 0xd3, 0x63, 0x41, + 0xfc, 0x81, 0x43, 0x1c, 0x6f, 0x7c, 0xba, 0xe8, 0xbd, 0x86, + 0x31, 0xd5, 0x70, 0x7f, 0xb5, 0x4a, 0x90, 0x3e, 0x84, 0xe1, + 0x71, 0xe0, 0x02, 0x99, 0xf4, 0x71, 0xe7, 0x02, 0xed, 0x36, + 0xaf, 0xde, 0x56, 0xc2, 0x90, 0xe0, 0xae, 0xc2, 0xf9, 0xaf, + 0x53, 0xc6, 0xd8, 0x62, 0x16, 0x32, 0x27, 0xfb, 0x6e, 0x9b, + 0x48, 0xc6, 0xea, 0xff, 0x6f, 0x78, 0x02, 0x22, 0x98, 0x2c, + 0x1f, 0xbf, 0xb0, 0x8e, 0xa9, 0x39, 0xbc, 0xdf, 0x17, 0xde, + 0xd7, 0x0e, 0xe1, 0x7a, 0x01, 0x0e, 0xc3, 0x87, 0xfc, 0xaa, + 0xe4, 0x6b, 0x0f, 0x5b, 0x0a, 0xf1, 0x18, 0x19, 0x8a, 0xe5, + 0x2c, 0x36, 0x9b, 0x40, 0x30, 0x99, 0x24, 0x94, 0x48, 0xd7, + 0x47, 0xb2, 0xaf, 0x6b, 0x8c, 0x40, 0x9e, 0x4d, 0x6d, 0x34, + 0x07, 0xc1, 0x26, 0x2f, 0xbb, 0x14, 0xf7, 0xbc, 0x36, 0x52, + 0xbd, 0x84, 0xfe, 0x4a, 0x9a, 0xf4, 0x8a, 0xdb, 0x34, 0x89, + 0xaa, 0xf1, 0x0d, 0x94, 0x0b, 0x92, 0xf4, 0x1c, 0xe4, 0x6c, + 0x79, 0x2d, 0x6e, 0xc0, 0x19, 0x0a, 0xd5, 0x55, 0x94, 0x14, + 0x05, 0x13, 0xc2, 0x62, 0x23, 0xb3, 0xd4, 0x26, 0xc4, 0x44, + 0x56, 0x7a, 0xcd, 0x1c, 0xea, 0xd4, 0x74, 0xb9, 0x36, 0x40, + 0x9f, 0x08, 0xfb, 0x49, 0x62, 0x05, 0x92, 0x98, 0xad, 0x1d, + 0x9f, 0x8a, 0x76, 0x8b, 0xd4, 0x0f, 0x21, 0x40, 0x76, 0xb6, + 0x16, 0x91, 0x45, 0x93, 0x66, 0xcc, 0x12, 0xea, 0x4d, 0xf4, + 0x09, 0xe2, 0xac, 0x33, 0xd0, 0x6f, 0x43, 0x51, 0x07, 0x3e, + 0xd7, 0x95, 0x2c, 0x1e, 0x1f, 0x0c, 0x24, 0xb3, 0x0e, 0x3a, + 0xef, 0x95, 0xf5, 0xeb, 0x77, 0xdd, 0x20, 0xf2, 0x35, 0x98, + 0xf2, 0xae, 0xa9, 0x66, 0xe6, 0x13, 0xef, 0x5d, 0x3a, 0x2d, + 0x66, 0xed, 0xe2, 0x1e, 0xe9, 0x32, 0x4a, 0x40, 0xbf, 0x37, + 0xc6, 0x70, 0x29, 0xd9, 0x8c, 0xa1, 0x61, 0x4a, 0x29, 0x3d, + 0xc7, 0x55, 0x9c, 0x94, 0x9e, 0xc9, 0x11, 0x45, 0x10, 0x28, + 0xa7, 0x27, 0xd1, 0xd3, 0xd0, 0x84, 0x79, 0xc7, 0xa9, 0xb0, + 0xf6, 0xaf, 0x45, 0x8c, 0x3c, 0xd4, 0xdf, 0x3b, 0xf7, 0x0d, + 0xa2, 0x4f, 0x13, 0x97, 0x78, 0x27, 0xf0, 0x48, 0xc0, 0xa5, + 0xab, 0x83, 0x01, 0x05, 0xd0, 0x12, 0xd7, 0x1e, 0x12, 0x3a, + 0x4e, 0x98, 0x77, 0xae, 0xba, 0xb1, 0x4e, 0xb5, 0x3b, 0x59, + 0xca, 0x6d, 0xa5, 0x11, 0x80, 0x91, 0x9c, 0x07, 0x69, 0x59, + 0x5a, 0x53, 0x70, 0x7c, 0x95, 0x97, 0x11, 0x6d, 0x66, 0x8d, + 0xa3, 0xbd, 0xbb, 0x2d, 0xb0, 0xbf, 0x9b, 0x10, 0xcb, 0xc7, + 0x0f, 0x5b, 0x7e, 0x67, 0xe2, 0xb0, 0x4b, 0xba, 0x10, 0x12, + 0xb9, 0xbc, 0x97, 0xfd, 0x48, 0xe4, 0x8a, 0xc1, 0x0f, 0xa1, + 0x30, 0x9d, 0x56, 0x20, 0x24, 0x1a, 0x7d, 0x5b, 0xa0, 0xb4, + 0xbe, 0x9d, 0x38, 0x4f, 0xb4, 0x56, 0xa8, 0x4d, 0x13, 0x7c, + 0x44, 0xe8, 0x84, 0x97, 0xeb, 0x78, 0x2c, 0x52, 0x85, 0xe4, + 0xa2, 0xf6, 0xf3, 0xd9, 0x71, 0x9e, 0xee, 0xb8, 0x11, 0x47, + 0xfb, 0xa9, 0x1b, 0xc7, 0x40, 0xc6, 0xe1, 0x19, 0x6d, 0x50, + 0xa1, 0x2a + }; + + if(target < CHIAKI_TARGET_PS4_10) + return CHIAKI_ERR_INVALID_DATA; + const uint8_t *keys_1 = chiaki_target_is_ps5(target) ? ps5_keys_1 : ps4_keys_1; + uint8_t wurzelbert = chiaki_target_is_ps5(target) ? -0x2d : 0x29; + for(size_t i=0; ibright[3] ^= (uint8_t)((pin >> 0x00) & 0xff); } -CHIAKI_EXPORT ChiakiErrorCode chiaki_rpcrypt_init_regist(ChiakiRPCrypt *rpcrypt, const uint8_t *ambassador, size_t key_0_off, uint32_t pin) +CHIAKI_EXPORT ChiakiErrorCode chiaki_rpcrypt_init_regist(ChiakiRPCrypt *rpcrypt, ChiakiTarget target, const uint8_t *ambassador, size_t key_0_off, uint32_t pin) { - static const uint8_t keys_0[] = { - 0xbe, 0xce, 0x5d, 0xf0, 0xc1, 0x7d, 0xb5, 0xd0, 0xcb, 0x30, - 0x13, 0x5d, 0xaa, 0x56, 0x23, 0xfb, 0xc4, 0xbc, 0xf1, 0x8f, - 0x38, 0x57, 0xfb, 0xd4, 0xd4, 0x3f, 0x26, 0x38, 0xb5, 0xce, - 0xed, 0x6a, 0x21, 0xbc, 0x38, 0xd0, 0x1e, 0x68, 0xcc, 0x7b, - 0x45, 0xd1, 0xbe, 0x42, 0x1a, 0x08, 0xaa, 0x16, 0xfd, 0xb0, - 0xc0, 0xf4, 0xda, 0x35, 0xe9, 0x12, 0xfd, 0x21, 0x07, 0x48, - 0x34, 0xc1, 0xfc, 0x9f, 0x8c, 0xb6, 0xcb, 0x5d, 0xb2, 0x9c, - 0x84, 0xe0, 0x1a, 0xfa, 0xa0, 0xc7, 0xeb, 0x3a, 0x93, 0xb3, - 0xb3, 0xf1, 0x15, 0xaf, 0x13, 0xbd, 0x21, 0xab, 0xea, 0x5b, - 0x80, 0x50, 0x6b, 0x31, 0x1d, 0x7c, 0x1d, 0x40, 0xba, 0x3c, - 0x56, 0x0e, 0xe7, 0x94, 0x3a, 0x5b, 0xa1, 0x40, 0x80, 0x74, - 0x0a, 0xad, 0x28, 0xcf, 0x47, 0xdf, 0x42, 0xa6, 0x69, 0xe9, - 0x5e, 0xbb, 0xc0, 0xc0, 0x0e, 0xb2, 0xc5, 0x8a, 0xee, 0x08, - 0x03, 0xd2, 0x84, 0xe5, 0x91, 0x00, 0x1d, 0x46, 0x06, 0x55, - 0x09, 0x9d, 0x39, 0x9f, 0xd8, 0xe7, 0xfd, 0xad, 0x9e, 0x93, - 0x97, 0xc5, 0xea, 0xe7, 0xa3, 0x10, 0xa7, 0xf2, 0xa2, 0x93, - 0x7f, 0x07, 0x04, 0xb4, 0xee, 0xbb, 0xbf, 0x88, 0x23, 0x9c, - 0x6e, 0xa7, 0x62, 0xb1, 0x4b, 0x67, 0x1e, 0xb8, 0x3b, 0x1f, - 0x64, 0x93, 0x5a, 0x99, 0xec, 0xda, 0xfd, 0x0c, 0x6a, 0xb7, - 0xfe, 0xe4, 0x12, 0x76, 0x32, 0x65, 0xb8, 0x41, 0x23, 0xd1, - 0x17, 0x09, 0x9c, 0x24, 0x2d, 0x5c, 0x9d, 0x12, 0x79, 0xde, - 0xa1, 0xce, 0x69, 0xac, 0xa4, 0xbc, 0x39, 0x2f, 0x57, 0x38, - 0x84, 0x61, 0x2d, 0x2a, 0xe8, 0x04, 0xf8, 0xd5, 0x9d, 0x0b, - 0xff, 0x7e, 0x56, 0x0c, 0xec, 0x87, 0x0a, 0x1e, 0xab, 0xdf, - 0x93, 0x81, 0x13, 0xee, 0xcf, 0x32, 0x02, 0x5a, 0xbf, 0xb0, - 0x17, 0xb7, 0xba, 0xb5, 0x7f, 0xf0, 0x01, 0x7b, 0xe1, 0xcb, - 0x39, 0x7e, 0x60, 0x6d, 0xa4, 0x75, 0x6e, 0x29, 0x92, 0x45, - 0xa6, 0x4f, 0x74, 0x00, 0x86, 0x78, 0x73, 0xbe, 0xfd, 0x3e, - 0xe0, 0xd1, 0x0c, 0x6c, 0x0b, 0x49, 0x09, 0x83, 0x6c, 0x85, - 0x8a, 0x1d, 0xcb, 0x16, 0xce, 0x81, 0x7c, 0x49, 0xc9, 0x2c, - 0x63, 0x61, 0xde, 0xe2, 0x3f, 0x98, 0xb2, 0x73, 0xf0, 0x9a, - 0xec, 0x7b, 0x7c, 0xf1, 0xc9, 0xe1, 0x7f, 0xa5, 0x19, 0x8b, - 0x4b, 0xe8, 0x38, 0xa4, 0x34, 0x7d, 0xf4, 0x28, 0xfe, 0x0d, - 0x4d, 0x11, 0x57, 0x0c, 0x95, 0xf1, 0xaf, 0xd7, 0x34, 0x80, - 0xf4, 0xeb, 0x9b, 0x50, 0xe6, 0x6a, 0x5d, 0xea, 0xce, 0x0c, - 0x85, 0x4e, 0xc5, 0x5b, 0x93, 0x44, 0xc4, 0x24, 0x98, 0x80, - 0xfc, 0xf7, 0x72, 0x9c, 0x31, 0x0b, 0xee, 0x89, 0x67, 0xb3, - 0xa2, 0x69, 0x4f, 0xb3, 0x79, 0x5a, 0x14, 0x02, 0x70, 0xed, - 0x50, 0x13, 0x75, 0x00, 0x6a, 0xf3, 0xc6, 0x05, 0x1a, 0x00, - 0x33, 0x34, 0xf5, 0xac, 0x9e, 0x04, 0xdb, 0xc2, 0x00, 0xb0, - 0x1b, 0xc4, 0xf3, 0x97, 0x9d, 0x7f, 0xbe, 0xb8, 0x23, 0x8d, - 0x99, 0xe7, 0xcb, 0x74, 0x37, 0x4c, 0x57, 0xec, 0xd2, 0x69, - 0x49, 0x46, 0x75, 0x74, 0xaf, 0x51, 0x40, 0xa4, 0x11, 0x7b, - 0xb3, 0x2f, 0x51, 0xda, 0xe2, 0xef, 0x33, 0x73, 0x12, 0x18, - 0x25, 0x39, 0x03, 0x09, 0xca, 0x49, 0xdc, 0x8e, 0xf1, 0x94, - 0xd7, 0x80, 0x17, 0x9e, 0x87, 0x46, 0xc1, 0x04, 0x78, 0xd1, - 0xe5, 0x3d, 0x25, 0x88, 0xec, 0x72, 0x3a, 0x28, 0x41, 0x68, - 0x14, 0x6e, 0x10, 0xe4, 0xc9, 0x57, 0x75, 0x90, 0xfe, 0x22, - 0x1a, 0x63, 0x8e, 0xf4, 0xb8, 0x8d, 0x1a, 0x36, 0xfd, 0xb6, - 0xcb, 0x72, 0xc2, 0x97, 0x52, 0x9f, 0x91, 0x72, 0x1b, 0x75, - 0x57, 0x90, 0x3b, 0xfd, 0x5a, 0x93, 0x8c, 0xdb, 0xfc, 0xa3, - 0x03, 0xdf + static const uint8_t ps4_keys_0[512] = { + 0xbe, 0xce, 0x5d, 0xf0, 0xc1, 0x7d, 0xb5, 0xd0, 0xcb, 0x30, + 0x13, 0x5d, 0xaa, 0x56, 0x23, 0xfb, 0xc4, 0xbc, 0xf1, 0x8f, + 0x38, 0x57, 0xfb, 0xd4, 0xd4, 0x3f, 0x26, 0x38, 0xb5, 0xce, + 0xed, 0x6a, 0x21, 0xbc, 0x38, 0xd0, 0x1e, 0x68, 0xcc, 0x7b, + 0x45, 0xd1, 0xbe, 0x42, 0x1a, 0x08, 0xaa, 0x16, 0xfd, 0xb0, + 0xc0, 0xf4, 0xda, 0x35, 0xe9, 0x12, 0xfd, 0x21, 0x07, 0x48, + 0x34, 0xc1, 0xfc, 0x9f, 0x8c, 0xb6, 0xcb, 0x5d, 0xb2, 0x9c, + 0x84, 0xe0, 0x1a, 0xfa, 0xa0, 0xc7, 0xeb, 0x3a, 0x93, 0xb3, + 0xb3, 0xf1, 0x15, 0xaf, 0x13, 0xbd, 0x21, 0xab, 0xea, 0x5b, + 0x80, 0x50, 0x6b, 0x31, 0x1d, 0x7c, 0x1d, 0x40, 0xba, 0x3c, + 0x56, 0x0e, 0xe7, 0x94, 0x3a, 0x5b, 0xa1, 0x40, 0x80, 0x74, + 0x0a, 0xad, 0x28, 0xcf, 0x47, 0xdf, 0x42, 0xa6, 0x69, 0xe9, + 0x5e, 0xbb, 0xc0, 0xc0, 0x0e, 0xb2, 0xc5, 0x8a, 0xee, 0x08, + 0x03, 0xd2, 0x84, 0xe5, 0x91, 0x00, 0x1d, 0x46, 0x06, 0x55, + 0x09, 0x9d, 0x39, 0x9f, 0xd8, 0xe7, 0xfd, 0xad, 0x9e, 0x93, + 0x97, 0xc5, 0xea, 0xe7, 0xa3, 0x10, 0xa7, 0xf2, 0xa2, 0x93, + 0x7f, 0x07, 0x04, 0xb4, 0xee, 0xbb, 0xbf, 0x88, 0x23, 0x9c, + 0x6e, 0xa7, 0x62, 0xb1, 0x4b, 0x67, 0x1e, 0xb8, 0x3b, 0x1f, + 0x64, 0x93, 0x5a, 0x99, 0xec, 0xda, 0xfd, 0x0c, 0x6a, 0xb7, + 0xfe, 0xe4, 0x12, 0x76, 0x32, 0x65, 0xb8, 0x41, 0x23, 0xd1, + 0x17, 0x09, 0x9c, 0x24, 0x2d, 0x5c, 0x9d, 0x12, 0x79, 0xde, + 0xa1, 0xce, 0x69, 0xac, 0xa4, 0xbc, 0x39, 0x2f, 0x57, 0x38, + 0x84, 0x61, 0x2d, 0x2a, 0xe8, 0x04, 0xf8, 0xd5, 0x9d, 0x0b, + 0xff, 0x7e, 0x56, 0x0c, 0xec, 0x87, 0x0a, 0x1e, 0xab, 0xdf, + 0x93, 0x81, 0x13, 0xee, 0xcf, 0x32, 0x02, 0x5a, 0xbf, 0xb0, + 0x17, 0xb7, 0xba, 0xb5, 0x7f, 0xf0, 0x01, 0x7b, 0xe1, 0xcb, + 0x39, 0x7e, 0x60, 0x6d, 0xa4, 0x75, 0x6e, 0x29, 0x92, 0x45, + 0xa6, 0x4f, 0x74, 0x00, 0x86, 0x78, 0x73, 0xbe, 0xfd, 0x3e, + 0xe0, 0xd1, 0x0c, 0x6c, 0x0b, 0x49, 0x09, 0x83, 0x6c, 0x85, + 0x8a, 0x1d, 0xcb, 0x16, 0xce, 0x81, 0x7c, 0x49, 0xc9, 0x2c, + 0x63, 0x61, 0xde, 0xe2, 0x3f, 0x98, 0xb2, 0x73, 0xf0, 0x9a, + 0xec, 0x7b, 0x7c, 0xf1, 0xc9, 0xe1, 0x7f, 0xa5, 0x19, 0x8b, + 0x4b, 0xe8, 0x38, 0xa4, 0x34, 0x7d, 0xf4, 0x28, 0xfe, 0x0d, + 0x4d, 0x11, 0x57, 0x0c, 0x95, 0xf1, 0xaf, 0xd7, 0x34, 0x80, + 0xf4, 0xeb, 0x9b, 0x50, 0xe6, 0x6a, 0x5d, 0xea, 0xce, 0x0c, + 0x85, 0x4e, 0xc5, 0x5b, 0x93, 0x44, 0xc4, 0x24, 0x98, 0x80, + 0xfc, 0xf7, 0x72, 0x9c, 0x31, 0x0b, 0xee, 0x89, 0x67, 0xb3, + 0xa2, 0x69, 0x4f, 0xb3, 0x79, 0x5a, 0x14, 0x02, 0x70, 0xed, + 0x50, 0x13, 0x75, 0x00, 0x6a, 0xf3, 0xc6, 0x05, 0x1a, 0x00, + 0x33, 0x34, 0xf5, 0xac, 0x9e, 0x04, 0xdb, 0xc2, 0x00, 0xb0, + 0x1b, 0xc4, 0xf3, 0x97, 0x9d, 0x7f, 0xbe, 0xb8, 0x23, 0x8d, + 0x99, 0xe7, 0xcb, 0x74, 0x37, 0x4c, 0x57, 0xec, 0xd2, 0x69, + 0x49, 0x46, 0x75, 0x74, 0xaf, 0x51, 0x40, 0xa4, 0x11, 0x7b, + 0xb3, 0x2f, 0x51, 0xda, 0xe2, 0xef, 0x33, 0x73, 0x12, 0x18, + 0x25, 0x39, 0x03, 0x09, 0xca, 0x49, 0xdc, 0x8e, 0xf1, 0x94, + 0xd7, 0x80, 0x17, 0x9e, 0x87, 0x46, 0xc1, 0x04, 0x78, 0xd1, + 0xe5, 0x3d, 0x25, 0x88, 0xec, 0x72, 0x3a, 0x28, 0x41, 0x68, + 0x14, 0x6e, 0x10, 0xe4, 0xc9, 0x57, 0x75, 0x90, 0xfe, 0x22, + 0x1a, 0x63, 0x8e, 0xf4, 0xb8, 0x8d, 0x1a, 0x36, 0xfd, 0xb6, + 0xcb, 0x72, 0xc2, 0x97, 0x52, 0x9f, 0x91, 0x72, 0x1b, 0x75, + 0x57, 0x90, 0x3b, 0xfd, 0x5a, 0x93, 0x8c, 0xdb, 0xfc, 0xa3, + 0x03, 0xdf }; + static const uint8_t ps5_keys_0[512] = { + 0x24, 0xd8, 0xc2, 0x69, 0x4c, 0x67, 0x78, 0x71, 0xee, 0x31, + 0xbd, 0x2b, 0x83, 0xb2, 0x1d, 0x61, 0xc9, 0xa7, 0x8e, 0xed, + 0x9a, 0xd3, 0x6a, 0x6b, 0x5c, 0xc8, 0x35, 0x79, 0xa7, 0x24, + 0xe2, 0x17, 0x06, 0x60, 0x2e, 0xdf, 0xf4, 0xdb, 0x27, 0x10, + 0x55, 0xd9, 0xea, 0x16, 0x4e, 0x90, 0x0c, 0xbf, 0x40, 0x6f, + 0x54, 0xa5, 0x31, 0x70, 0x2d, 0x5d, 0x1e, 0x27, 0xdf, 0x37, + 0x40, 0xba, 0x9d, 0x5d, 0xff, 0xe1, 0x05, 0x70, 0x80, 0xd4, + 0xb7, 0xc2, 0x96, 0x7f, 0x2f, 0x42, 0xeb, 0x5a, 0x08, 0xde, + 0xc1, 0xb5, 0x52, 0x15, 0xf6, 0xb5, 0xf2, 0xd9, 0x69, 0xa5, + 0xc7, 0xc4, 0x7f, 0x46, 0x64, 0xa4, 0xfd, 0x46, 0x98, 0xa7, + 0xe1, 0x2a, 0x8e, 0x6f, 0xaf, 0x65, 0x42, 0x28, 0xb9, 0xc2, + 0x6f, 0x3e, 0xe3, 0xe4, 0x4e, 0xe4, 0x5b, 0x9d, 0x60, 0x10, + 0xb8, 0x5a, 0xb0, 0x7d, 0x04, 0x0c, 0x4c, 0x24, 0x78, 0xbd, + 0xb8, 0xba, 0xdb, 0x8f, 0xe3, 0xa0, 0x75, 0x6d, 0x28, 0xc2, + 0x33, 0x5b, 0x32, 0x83, 0xdd, 0x51, 0xb0, 0xa5, 0x8d, 0x09, + 0x66, 0xe4, 0x5c, 0xb8, 0x70, 0x0b, 0xe6, 0x82, 0x14, 0xb6, + 0xd2, 0xb0, 0xc2, 0xe0, 0x55, 0xf3, 0x84, 0xad, 0x9d, 0x3a, + 0xf8, 0x77, 0xf5, 0x9d, 0x9a, 0xa9, 0x7d, 0xf1, 0x45, 0x1b, + 0x9b, 0x55, 0x25, 0xd8, 0xc1, 0xff, 0x03, 0xa5, 0x48, 0x0b, + 0x1b, 0x19, 0x0c, 0xbd, 0xe0, 0xcd, 0x48, 0xf3, 0x2c, 0x99, + 0x19, 0xd6, 0xb8, 0xbb, 0xd6, 0x35, 0x43, 0x6f, 0x71, 0xe3, + 0xef, 0x3e, 0x97, 0xb8, 0xe9, 0x40, 0xa8, 0x47, 0xe0, 0xe0, + 0x01, 0x16, 0x9d, 0xa7, 0xe5, 0x94, 0x4b, 0x1d, 0xd2, 0x80, + 0xa2, 0x7f, 0xf2, 0x98, 0x10, 0x38, 0x0d, 0xb8, 0x56, 0xc3, + 0x7a, 0x4b, 0x4c, 0x85, 0xec, 0x2f, 0x23, 0x89, 0xaf, 0xd5, + 0xba, 0x9a, 0xad, 0xb0, 0x61, 0x9c, 0x51, 0xb4, 0x6d, 0x02, + 0x49, 0x26, 0xa4, 0x34, 0x84, 0x20, 0x35, 0x30, 0x23, 0x0a, + 0x47, 0x14, 0x32, 0x1a, 0x96, 0x0e, 0xe8, 0x0f, 0x96, 0x96, + 0xd4, 0xba, 0x68, 0x3a, 0x67, 0x15, 0x74, 0xe0, 0xd6, 0x60, + 0x4c, 0x68, 0x50, 0x73, 0x14, 0x2f, 0x11, 0x59, 0xac, 0xc8, + 0x32, 0xd1, 0xdb, 0x4c, 0x8a, 0x94, 0x75, 0x33, 0x61, 0xd1, + 0xd4, 0xfd, 0xaa, 0x6a, 0x61, 0x68, 0xd8, 0xae, 0x31, 0x4f, + 0xb8, 0x07, 0x7b, 0x27, 0x0f, 0xf9, 0x0b, 0xb0, 0xc2, 0x64, + 0xb3, 0x72, 0xea, 0x8b, 0x87, 0x40, 0x09, 0xb4, 0x82, 0xb4, + 0xad, 0x76, 0xf9, 0x36, 0x05, 0x60, 0x89, 0xc8, 0x20, 0xeb, + 0xa5, 0xf1, 0x51, 0x0b, 0x27, 0xa7, 0xf0, 0x76, 0x84, 0x96, + 0xeb, 0xb1, 0x2e, 0xc2, 0x85, 0x28, 0xbc, 0x48, 0x34, 0xd4, + 0x01, 0x8d, 0x5b, 0x25, 0x54, 0xe0, 0xc4, 0x4f, 0xa0, 0xfa, + 0x99, 0x8d, 0x6d, 0x7a, 0x64, 0xb1, 0xa9, 0x5d, 0xa4, 0xf9, + 0xf5, 0x22, 0xeb, 0x9a, 0xf4, 0xa8, 0x7a, 0x78, 0x4b, 0x7f, + 0xe2, 0x8b, 0x04, 0x50, 0x43, 0x7d, 0x26, 0x2d, 0x19, 0x98, + 0x38, 0x6a, 0x4f, 0x2d, 0x30, 0x15, 0x2e, 0x4f, 0xcd, 0xb9, + 0xce, 0x9e, 0x8d, 0x12, 0xc9, 0xfe, 0x33, 0x8b, 0x84, 0xce, + 0x5b, 0x40, 0xe3, 0x7f, 0x72, 0x6d, 0x6c, 0x8a, 0x6a, 0x9e, + 0x54, 0xf1, 0xe3, 0x64, 0x5d, 0x6e, 0x7f, 0xac, 0x1a, 0xe7, + 0xf7, 0xfa, 0x00, 0x22, 0xed, 0x2b, 0x23, 0xfa, 0x58, 0xc5, + 0xeb, 0x44, 0x92, 0x5d, 0xcc, 0xaa, 0x82, 0x9f, 0x23, 0xfb, + 0xa6, 0xc9, 0x65, 0x2a, 0xe0, 0x79, 0x12, 0x65, 0x2c, 0x34, + 0xc5, 0x23, 0x16, 0xc9, 0xcc, 0x05, 0x30, 0xf3, 0x96, 0x0b, + 0x90, 0x67, 0x1a, 0xa7, 0x69, 0x4c, 0x3e, 0x43, 0x24, 0x9d, + 0x4e, 0x68, 0xbd, 0x8b, 0x75, 0x6e, 0x9d, 0x07, 0x6f, 0x1a, + 0x6a, 0xba + }; + + if(target < CHIAKI_TARGET_PS4_10) + return CHIAKI_ERR_INVALID_DATA; + const uint8_t *keys_0 = chiaki_target_is_ps5(target) ? ps5_keys_0 : ps4_keys_0; + if(key_0_off >= 0x20) return CHIAKI_ERR_INVALID_DATA; - rpcrypt->target = CHIAKI_TARGET_PS4_10; // representative, might not be the actual version - + rpcrypt->target = target; memcpy(rpcrypt->ambassador, ambassador, sizeof(rpcrypt->ambassador)); for(size_t i=0; i Date: Fri, 11 Dec 2020 19:44:40 +0100 Subject: [PATCH 094/237] Finish PS5 Regist --- gui/include/host.h | 12 ++++--- gui/src/host.cpp | 23 +++++++------ gui/src/main.cpp | 4 +-- gui/src/manualhostdialog.cpp | 4 +-- gui/src/registdialog.cpp | 8 ++--- gui/src/serveritemwidget.cpp | 4 +-- gui/src/settings.cpp | 63 ++++++++++++++++++++++++++++++++++-- gui/src/settingsdialog.cpp | 4 +-- lib/include/chiaki/regist.h | 5 +-- lib/src/regist.c | 20 ++++++------ lib/src/session.c | 26 +++++++++------ 11 files changed, 122 insertions(+), 51 deletions(-) diff --git a/gui/include/host.h b/gui/include/host.h index 0e403ce..8ff292e 100644 --- a/gui/include/host.h +++ b/gui/include/host.h @@ -38,12 +38,13 @@ static bool operator<(const HostMAC &a, const HostMAC &b) { return a.GetValue() class RegisteredHost { private: + ChiakiTarget target; QString ap_ssid; QString ap_bssid; QString ap_key; QString ap_name; - HostMAC ps4_mac; - QString ps4_nickname; + HostMAC server_mac; + QString server_nickname; char rp_regist_key[CHIAKI_SESSION_AUTH_SIZE]; uint32_t rp_key_type; uint8_t rp_key[0x10]; @@ -54,8 +55,9 @@ class RegisteredHost RegisteredHost(const ChiakiRegisteredHost &chiaki_host); - const HostMAC &GetPS4MAC() const { return ps4_mac; } - const QString &GetPS4Nickname() const { return ps4_nickname; } + ChiakiTarget GetTarget() const { return target; } + const HostMAC &GetServerMAC() const { return server_mac; } + const QString &GetServerNickname() const { return server_nickname; } const QByteArray GetRPRegistKey() const { return QByteArray(rp_regist_key, sizeof(rp_regist_key)); } const QByteArray GetRPKey() const { return QByteArray((const char *)rp_key, sizeof(rp_key)); } @@ -81,7 +83,7 @@ class ManualHost bool GetRegistered() const { return registered; } HostMAC GetMAC() const { return registered_mac; } - void Register(const RegisteredHost ®istered_host) { this->registered = true; this->registered_mac = registered_host.GetPS4MAC(); } + void Register(const RegisteredHost ®istered_host) { this->registered = true; this->registered_mac = registered_host.GetServerMAC(); } void SaveToSettings(QSettings *settings) const; static ManualHost LoadFromSettings(QSettings *settings); diff --git a/gui/src/host.cpp b/gui/src/host.cpp index 8a5f8ce..c6f3e63 100644 --- a/gui/src/host.cpp +++ b/gui/src/host.cpp @@ -15,8 +15,8 @@ RegisteredHost::RegisteredHost(const RegisteredHost &o) ap_bssid(o.ap_bssid), ap_key(o.ap_key), ap_name(o.ap_name), - ps4_mac(o.ps4_mac), - ps4_nickname(o.ps4_nickname), + server_mac(o.server_mac), + server_nickname(o.server_nickname), rp_key_type(o.rp_key_type) { memcpy(rp_regist_key, o.rp_regist_key, sizeof(rp_regist_key)); @@ -24,13 +24,14 @@ RegisteredHost::RegisteredHost(const RegisteredHost &o) } RegisteredHost::RegisteredHost(const ChiakiRegisteredHost &chiaki_host) - : ps4_mac(chiaki_host.ps4_mac) + : server_mac(chiaki_host.server_mac) { + target = chiaki_host.target; ap_ssid = chiaki_host.ap_ssid; ap_bssid = chiaki_host.ap_bssid; ap_key = chiaki_host.ap_key; ap_name = chiaki_host.ap_name; - ps4_nickname = chiaki_host.ps4_nickname; + server_nickname = chiaki_host.server_nickname; memcpy(rp_regist_key, chiaki_host.rp_regist_key, sizeof(rp_regist_key)); rp_key_type = chiaki_host.rp_key_type; memcpy(rp_key, chiaki_host.rp_key, sizeof(rp_key)); @@ -38,12 +39,13 @@ RegisteredHost::RegisteredHost(const ChiakiRegisteredHost &chiaki_host) void RegisteredHost::SaveToSettings(QSettings *settings) const { + settings->setValue("target", (int)target); settings->setValue("ap_ssid", ap_ssid); settings->setValue("ap_bssid", ap_bssid); settings->setValue("ap_key", ap_key); settings->setValue("ap_name", ap_name); - settings->setValue("ps4_nickname", ps4_nickname); - settings->setValue("ps4_mac", QByteArray((const char *)ps4_mac.GetMAC(), 6)); + settings->setValue("server_nickname", server_nickname); + settings->setValue("server_mac", QByteArray((const char *)server_mac.GetMAC(), 6)); settings->setValue("rp_regist_key", QByteArray(rp_regist_key, sizeof(rp_regist_key))); settings->setValue("rp_key_type", rp_key_type); settings->setValue("rp_key", QByteArray((const char *)rp_key, sizeof(rp_key))); @@ -52,14 +54,15 @@ void RegisteredHost::SaveToSettings(QSettings *settings) const RegisteredHost RegisteredHost::LoadFromSettings(QSettings *settings) { RegisteredHost r; + r.target = (ChiakiTarget)settings->value("target").toInt(); r.ap_ssid = settings->value("ap_ssid").toString(); r.ap_bssid = settings->value("ap_bssid").toString(); r.ap_key = settings->value("ap_key").toString(); r.ap_name = settings->value("ap_name").toString(); - r.ps4_nickname = settings->value("ps4_nickname").toString(); - auto ps4_mac = settings->value("ps4_mac").toByteArray(); - if(ps4_mac.size() == 6) - r.ps4_mac = HostMAC((const uint8_t *)ps4_mac.constData()); + r.server_nickname = settings->value("server_nickname").toString(); + auto server_mac = settings->value("server_mac").toByteArray(); + if(server_mac.size() == 6) + r.server_mac = HostMAC((const uint8_t *)server_mac.constData()); auto rp_regist_key = settings->value("rp_regist_key").toByteArray(); if(rp_regist_key.size() == sizeof(r.rp_regist_key)) memcpy(r.rp_regist_key, rp_regist_key.constData(), sizeof(r.rp_regist_key)); diff --git a/gui/src/main.cpp b/gui/src/main.cpp index 4ec7871..ac6e4ce 100644 --- a/gui/src/main.cpp +++ b/gui/src/main.cpp @@ -112,7 +112,7 @@ int real_main(int argc, char *argv[]) if(args[0] == "list") { for(const auto &host : settings.GetRegisteredHosts()) - printf("Host: %s \n", host.GetPS4Nickname().toLocal8Bit().constData()); + printf("Host: %s \n", host.GetServerNickname().toLocal8Bit().constData()); return 0; } if(args[0] == "stream") @@ -131,7 +131,7 @@ int real_main(int argc, char *argv[]) parser.showHelp(1); for(const auto &temphost : settings.GetRegisteredHosts()) { - if(temphost.GetPS4Nickname() == args[1]) + if(temphost.GetServerNickname() == args[1]) { morning = temphost.GetRPKey(); regist_key = temphost.GetRPRegistKey(); diff --git a/gui/src/manualhostdialog.cpp b/gui/src/manualhostdialog.cpp index 291bb32..d184d3a 100644 --- a/gui/src/manualhostdialog.cpp +++ b/gui/src/manualhostdialog.cpp @@ -38,7 +38,7 @@ ManualHostDialog::ManualHostDialog(Settings *settings, int id, QWidget *parent) registered_host_combo_box->addItem(tr("Register on first Connection")); auto registered_hosts = settings->GetRegisteredHosts(); for(const auto ®istered_host : registered_hosts) - registered_host_combo_box->addItem(QString("%1 (%2)").arg(registered_host.GetPS4MAC().ToString(), registered_host.GetPS4Nickname()), QVariant::fromValue(registered_host.GetPS4MAC())); + registered_host_combo_box->addItem(QString("%1 (%2)").arg(registered_host.GetServerMAC().ToString(), registered_host.GetServerNickname()), QVariant::fromValue(registered_host.GetServerMAC())); form_layout->addRow(tr("Registered Console:"), registered_host_combo_box); button_box = new QDialogButtonBox(QDialogButtonBox::Save | QDialogButtonBox::Discard, this); @@ -74,4 +74,4 @@ void ManualHostDialog::accept() ManualHost host(host_id, host_edit->text().trimmed(), registered, registered_mac); settings->SetManualHost(host); QDialog::accept(); -} \ No newline at end of file +} diff --git a/gui/src/registdialog.cpp b/gui/src/registdialog.cpp index c101bf3..4829da1 100644 --- a/gui/src/registdialog.cpp +++ b/gui/src/registdialog.cpp @@ -205,14 +205,14 @@ void RegistExecuteDialog::Finished() void RegistExecuteDialog::Success(RegisteredHost host) { - CHIAKI_LOGI(&log, "Successfully registered %s", host.GetPS4Nickname().toLocal8Bit().constData()); + CHIAKI_LOGI(&log, "Successfully registered %s", host.GetServerNickname().toLocal8Bit().constData()); this->registered_host = host; - if(settings->GetRegisteredHostRegistered(host.GetPS4MAC())) + if(settings->GetRegisteredHostRegistered(host.GetServerMAC())) { int r = QMessageBox::question(this, tr("Console already registered"), - tr("The console with ID %1 has already been registered. Should the previous record be overwritten?").arg(host.GetPS4MAC().ToString())); + tr("The console with ID %1 has already been registered. Should the previous record be overwritten?").arg(host.GetServerMAC().ToString())); if(r == QMessageBox::No) { accept(); @@ -222,7 +222,7 @@ void RegistExecuteDialog::Success(RegisteredHost host) settings->AddRegisteredHost(host); - QMessageBox::information(this, tr("Console registered"), tr("The Console %1 with ID %2 has been successfully registered!").arg(host.GetPS4Nickname(), host.GetPS4MAC().ToString())); + QMessageBox::information(this, tr("Console registered"), tr("The Console %1 with ID %2 has been successfully registered!").arg(host.GetServerNickname(), host.GetServerMAC().ToString())); accept(); } diff --git a/gui/src/serveritemwidget.cpp b/gui/src/serveritemwidget.cpp index f82ae6a..ef7448e 100644 --- a/gui/src/serveritemwidget.cpp +++ b/gui/src/serveritemwidget.cpp @@ -76,7 +76,7 @@ void ServerItemWidget::Update(const DisplayServer &display_server) if(display_server.discovered || display_server.registered) { - top_text += (display_server.discovered ? display_server.discovery_host.host_name : display_server.registered_host.GetPS4Nickname()) + "\n"; + top_text += (display_server.discovered ? display_server.discovery_host.host_name : display_server.registered_host.GetServerNickname()) + "\n"; } top_text += tr("Address: %1").arg(display_server.GetHostAddr()); @@ -84,7 +84,7 @@ void ServerItemWidget::Update(const DisplayServer &display_server) if(display_server.discovered || display_server.registered) { top_text += "\n" + tr("ID: %1 (%2)").arg( - display_server.discovered ? display_server.discovery_host.GetHostMAC().ToString() : display_server.registered_host.GetPS4MAC().ToString(), + display_server.discovered ? display_server.discovery_host.GetHostMAC().ToString() : display_server.registered_host.GetServerMAC().ToString(), display_server.registered ? tr("registered") : tr("unregistered")); } diff --git a/gui/src/settings.cpp b/gui/src/settings.cpp index c6bcd7b..e08ec57 100644 --- a/gui/src/settings.cpp +++ b/gui/src/settings.cpp @@ -5,10 +5,67 @@ #include -#define SETTINGS_VERSION 1 +#define SETTINGS_VERSION 2 + +static void MigrateSettingsTo2(QSettings *settings) +{ + QList> hosts; + int count = settings->beginReadArray("registered_hosts"); + for(int i=0; isetArrayIndex(i); + QMap host; + for(QString k : settings->allKeys()) + host[k] = settings->value(k); + } + settings->endArray(); + settings->beginWriteArray("registered_hosts"); + int i=0; + for(const auto &host : hosts) + { + settings->setArrayIndex(i); + settings->setValue("target", (int)CHIAKI_TARGET_PS4_10); + for(auto it = host.constBegin(); it != host.constEnd(); it++) + { + QString k = it.key(); + if(k == "ps4_nickname") + k = "server_nickname"; + else if(k == "ps4_mac") + k = "server_mac"; + settings->setValue(k, it.value()); + } + i++; + } + settings->endArray(); +} + +static void MigrateSettings(QSettings *settings) +{ + int version_prev = settings->value("version", 0).toInt(); + if(version_prev < 1) + return; + if(version_prev > SETTINGS_VERSION) + { + CHIAKI_LOGE(NULL, "Settings version %d is higher than application one (%d)", version_prev, SETTINGS_VERSION); + return; + } + while(version_prev < 1) + { + version_prev++; + switch(version_prev) + { + case 2: + MigrateSettingsTo2(settings); + break; + default: + break; + } + } +} Settings::Settings(QObject *parent) : QObject(parent) { + MigrateSettings(&settings); manual_hosts_id_next = 0; settings.setValue("version", SETTINGS_VERSION); LoadRegisteredHosts(); @@ -183,7 +240,7 @@ void Settings::LoadRegisteredHosts() { settings.setArrayIndex(i); RegisteredHost host = RegisteredHost::LoadFromSettings(&settings); - registered_hosts[host.GetPS4MAC()] = host; + registered_hosts[host.GetServerMAC()] = host; } settings.endArray(); } @@ -203,7 +260,7 @@ void Settings::SaveRegisteredHosts() void Settings::AddRegisteredHost(const RegisteredHost &host) { - registered_hosts[host.GetPS4MAC()] = host; + registered_hosts[host.GetServerMAC()] = host; SaveRegisteredHosts(); emit RegisteredHostsUpdated(); } diff --git a/gui/src/settingsdialog.cpp b/gui/src/settingsdialog.cpp index 73dd350..4411343 100644 --- a/gui/src/settingsdialog.cpp +++ b/gui/src/settingsdialog.cpp @@ -348,8 +348,8 @@ void SettingsDialog::UpdateRegisteredHosts() auto hosts = settings->GetRegisteredHosts(); for(const auto &host : hosts) { - auto item = new QListWidgetItem(QString("%1 (%2)").arg(host.GetPS4MAC().ToString(), host.GetPS4Nickname())); - item->setData(Qt::UserRole, QVariant::fromValue(host.GetPS4MAC())); + auto item = new QListWidgetItem(QString("%1 (%2)").arg(host.GetServerMAC().ToString(), host.GetServerNickname())); + item->setData(Qt::UserRole, QVariant::fromValue(host.GetServerMAC())); registered_hosts_list_widget->addItem(item); } } diff --git a/lib/include/chiaki/regist.h b/lib/include/chiaki/regist.h index c12d57f..9ad1efc 100644 --- a/lib/include/chiaki/regist.h +++ b/lib/include/chiaki/regist.h @@ -37,12 +37,13 @@ typedef struct chiaki_regist_info_t typedef struct chiaki_registered_host_t { + ChiakiTarget target; char ap_ssid[0x30]; char ap_bssid[0x20]; char ap_key[0x50]; char ap_name[0x20]; - uint8_t ps4_mac[6]; - char ps4_nickname[0x20]; + uint8_t server_mac[6]; + char server_nickname[0x20]; char rp_regist_key[CHIAKI_SESSION_AUTH_SIZE]; // must be completely filled (pad with \0) uint32_t rp_key_type; uint8_t rp_key[0x10]; diff --git a/lib/src/regist.c b/lib/src/regist.c index b2cee08..47c6ab1 100644 --- a/lib/src/regist.c +++ b/lib/src/regist.c @@ -633,20 +633,22 @@ static ChiakiErrorCode regist_parse_response_payload(ChiakiRegist *regist, Chiak } memset(host, 0, sizeof(*host)); + host->target = regist->info.target; bool mac_found = false; bool regist_key_found = false; bool key_found = false; + bool ps5 = chiaki_target_is_ps5(regist->info.target); for(ChiakiHttpHeader *header=headers; header; header=header->next) { #define COPY_STRING(name, key_str) \ - if(strcmp(header->key, key_str) == 0) \ + if(strcmp(header->key, (key_str)) == 0) \ { \ size_t len = strlen(header->value); \ if(len >= sizeof(host->name)) \ { \ - CHIAKI_LOGE(regist->log, "Regist value for " key_str " in response is too long"); \ + CHIAKI_LOGE(regist->log, "Regist value for %s in response is too long", (key_str)); \ continue; \ } \ memcpy(host->name, header->value, len); \ @@ -657,10 +659,10 @@ static ChiakiErrorCode regist_parse_response_payload(ChiakiRegist *regist, Chiak COPY_STRING(ap_bssid, "AP-Bssid") COPY_STRING(ap_key, "AP-Key") COPY_STRING(ap_name, "AP-Name") - COPY_STRING(ps4_nickname, "PS4-Nickname") + COPY_STRING(server_nickname, ps5 ? "PS5-Nickname" : "PS4-Nickname") #undef COPY_STRING - if(strcmp(header->key, "PS4-RegistKey") == 0) + if(strcmp(header->key, ps5 ? "PS5-RegistKey" : "PS4-RegistKey") == 0) { memset(host->rp_regist_key, 0, sizeof(host->rp_regist_key)); size_t buf_size = sizeof(host->rp_regist_key); @@ -693,14 +695,14 @@ static ChiakiErrorCode regist_parse_response_payload(ChiakiRegist *regist, Chiak key_found = true; } } - else if(strcmp(header->key, "PS4-Mac") == 0) + else if(strcmp(header->key, ps5 ? "PS5-Mac" : "PS4-Mac") == 0) { - size_t buf_size = sizeof(host->ps4_mac); - err = parse_hex((uint8_t *)host->ps4_mac, &buf_size, header->value, strlen(header->value)); - if(err != CHIAKI_ERR_SUCCESS || buf_size != sizeof(host->ps4_mac)) + size_t buf_size = sizeof(host->server_mac); + err = parse_hex((uint8_t *)host->server_mac, &buf_size, header->value, strlen(header->value)); + if(err != CHIAKI_ERR_SUCCESS || buf_size != sizeof(host->server_mac)) { CHIAKI_LOGE(regist->log, "Regist received invalid MAC Address in response"); - memset(host->ps4_mac, 0, sizeof(host->ps4_mac)); + memset(host->server_mac, 0, sizeof(host->server_mac)); } else { diff --git a/lib/src/session.c b/lib/src/session.c index ab33d97..38c8950 100644 --- a/lib/src/session.c +++ b/lib/src/session.c @@ -31,7 +31,7 @@ #define SESSION_EXPECT_TIMEOUT_MS 5000 static void *session_thread_func(void *arg); -static bool session_thread_request_session(ChiakiSession *session, ChiakiTarget *target_out); +static ChiakiErrorCode session_thread_request_session(ChiakiSession *session, ChiakiTarget *target_out); const char *chiaki_rp_application_reason_string(uint32_t reason) { @@ -369,20 +369,20 @@ static void *session_thread_func(void *arg) CHIAKI_LOGI(session->log, "Starting session request"); ChiakiTarget server_target = session->connect_info.ps5 ? CHIAKI_TARGET_PS5_UNKNOWN : CHIAKI_TARGET_PS4_UNKNOWN; - success = session_thread_request_session(session, &server_target); + success = session_thread_request_session(session, &server_target) == CHIAKI_ERR_SUCCESS; if(!success && chiaki_target_is_unknown(server_target)) { CHIAKI_LOGI(session->log, "Attempting to re-request session with Server's RP-Version"); session->target = server_target; - success = session_thread_request_session(session, &server_target); + success = session_thread_request_session(session, &server_target) == CHIAKI_ERR_SUCCESS; } if(!success && chiaki_target_is_unknown(server_target)) { CHIAKI_LOGI(session->log, "Attempting to re-request session even harder with Server's RP-Version!!!"); session->target = server_target; - success = session_thread_request_session(session, NULL); + success = session_thread_request_session(session, NULL) == CHIAKI_ERR_SUCCESS; } if(!success) @@ -569,7 +569,7 @@ static void parse_session_response(SessionResponse *response, ChiakiHttpResponse /** * @param target_out if NULL, version mismatch means to fail the entire session, otherwise report the target here */ -static bool session_thread_request_session(ChiakiSession *session, ChiakiTarget *target_out) +static ChiakiErrorCode session_thread_request_session(ChiakiSession *session, ChiakiTarget *target_out) { chiaki_socket_t session_sock = CHIAKI_INVALID_SOCKET; for(struct addrinfo *ai=session->connect_info.host_addrinfos; ai; ai=ai->ai_next) @@ -690,6 +690,12 @@ static bool session_thread_request_session(ChiakiSession *session, ChiakiTarget } const char *rp_version_str = chiaki_rp_version_string(session->target); + if(!rp_version_str) + { + CHIAKI_LOGE(session->log, "Failed to get version for target, probably invalid target value"); + session->quit_reason = CHIAKI_QUIT_REASON_SESSION_REQUEST_UNKNOWN; + return CHIAKI_ERR_INVALID_DATA; + } char buf[512]; int request_len = snprintf(buf, sizeof(buf), session_request_fmt, @@ -698,7 +704,7 @@ static bool session_thread_request_session(ChiakiSession *session, ChiakiTarget { CHIAKI_SOCKET_CLOSE(session_sock); session->quit_reason = CHIAKI_QUIT_REASON_SESSION_REQUEST_UNKNOWN; - return false; + return CHIAKI_ERR_UNKNOWN; } CHIAKI_LOGI(session->log, "Sending session request"); @@ -710,7 +716,7 @@ static bool session_thread_request_session(ChiakiSession *session, ChiakiTarget CHIAKI_LOGE(session->log, "Failed to send session request"); CHIAKI_SOCKET_CLOSE(session_sock); session->quit_reason = CHIAKI_QUIT_REASON_SESSION_REQUEST_UNKNOWN; - return false; + return CHIAKI_ERR_NETWORK; } size_t header_size; @@ -731,7 +737,7 @@ static bool session_thread_request_session(ChiakiSession *session, ChiakiTarget session->quit_reason = CHIAKI_QUIT_REASON_SESSION_REQUEST_UNKNOWN; } CHIAKI_SOCKET_CLOSE(session_sock); - return false; + return CHIAKI_ERR_NETWORK; } ChiakiHttpResponse http_response; @@ -743,7 +749,7 @@ static bool session_thread_request_session(ChiakiSession *session, ChiakiTarget CHIAKI_LOGE(session->log, "Failed to parse session request response"); CHIAKI_SOCKET_CLOSE(session_sock); session->quit_reason = CHIAKI_QUIT_REASON_SESSION_REQUEST_UNKNOWN; - return false; + return CHIAKI_ERR_NETWORK; } SessionResponse response; @@ -802,7 +808,7 @@ static bool session_thread_request_session(ChiakiSession *session, ChiakiTarget chiaki_http_response_fini(&http_response); CHIAKI_SOCKET_CLOSE(session_sock); - return response.success; + return response.success ? CHIAKI_ERR_SUCCESS : CHIAKI_ERR_UNKNOWN; } CHIAKI_EXPORT ChiakiErrorCode chiaki_session_goto_bed(ChiakiSession *session) From aa9939e93c77f815e41fe6c6652f9b0541ca8c4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Wed, 16 Dec 2020 18:47:50 +0100 Subject: [PATCH 095/237] Preliminary PS5 Streaming --- .gitignore | 1 + gui/include/streamsession.h | 3 +- gui/src/host.cpp | 3 +- gui/src/main.cpp | 3 +- gui/src/mainwindow.cpp | 2 +- gui/src/streamsession.cpp | 4 +- lib/include/chiaki/common.h | 9 +- lib/include/chiaki/session.h | 1 + lib/src/ctrl.c | 50 ++- lib/src/rpcrypt.c | 788 ++++++++++++++++++++++++++++++++++- lib/src/session.c | 16 +- 11 files changed, 848 insertions(+), 32 deletions(-) diff --git a/.gitignore b/.gitignore index 9977c53..0b9eccd 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,4 @@ compile_commands.json .gdb_history chiaki.conf /appimage +.cache/ diff --git a/gui/include/streamsession.h b/gui/include/streamsession.h index 3be765d..aad26a3 100644 --- a/gui/include/streamsession.h +++ b/gui/include/streamsession.h @@ -45,6 +45,7 @@ struct StreamSessionConnectInfo QString audio_out_device; uint32_t log_level_mask; QString log_file; + ChiakiTarget target; QString host; QByteArray regist_key; QByteArray morning; @@ -53,7 +54,7 @@ struct StreamSessionConnectInfo bool fullscreen; bool enable_keyboard; - StreamSessionConnectInfo(Settings *settings, QString host, QByteArray regist_key, QByteArray morning, bool fullscreen); + StreamSessionConnectInfo(Settings *settings, ChiakiTarget target, QString host, QByteArray regist_key, QByteArray morning, bool fullscreen); }; class StreamSession : public QObject diff --git a/gui/src/host.cpp b/gui/src/host.cpp index c6f3e63..9eab756 100644 --- a/gui/src/host.cpp +++ b/gui/src/host.cpp @@ -11,7 +11,8 @@ RegisteredHost::RegisteredHost() } RegisteredHost::RegisteredHost(const RegisteredHost &o) - : ap_ssid(o.ap_ssid), + : target(o.target), + ap_ssid(o.ap_ssid), ap_bssid(o.ap_bssid), ap_key(o.ap_key), ap_name(o.ap_name), diff --git a/gui/src/main.cpp b/gui/src/main.cpp index ac6e4ce..e4fad7c 100644 --- a/gui/src/main.cpp +++ b/gui/src/main.cpp @@ -162,7 +162,8 @@ int real_main(int argc, char *argv[]) return 1; } } - StreamSessionConnectInfo connect_info(&settings, host, regist_key, morning, parser.isSet(fullscreen_option)); + // TODO: target here + StreamSessionConnectInfo connect_info(&settings, CHIAKI_TARGET_PS4_10, host, regist_key, morning, parser.isSet(fullscreen_option)); return RunStream(app, connect_info); } #ifdef CHIAKI_ENABLE_CLI diff --git a/gui/src/mainwindow.cpp b/gui/src/mainwindow.cpp index 2a10d50..1da8eb1 100644 --- a/gui/src/mainwindow.cpp +++ b/gui/src/mainwindow.cpp @@ -248,7 +248,7 @@ void MainWindow::ServerItemWidgetTriggered() } QString host = server.GetHostAddr(); - StreamSessionConnectInfo info(settings, host, server.registered_host.GetRPRegistKey(), server.registered_host.GetRPKey(), false); + StreamSessionConnectInfo info(settings, server.registered_host.GetTarget(), host, server.registered_host.GetRPRegistKey(), server.registered_host.GetRPKey(), false); new StreamWindow(info); } else diff --git a/gui/src/streamsession.cpp b/gui/src/streamsession.cpp index 3c8488b..396eb0b 100644 --- a/gui/src/streamsession.cpp +++ b/gui/src/streamsession.cpp @@ -14,7 +14,7 @@ #define SETSU_UPDATE_INTERVAL_MS 4 -StreamSessionConnectInfo::StreamSessionConnectInfo(Settings *settings, QString host, QByteArray regist_key, QByteArray morning, bool fullscreen) +StreamSessionConnectInfo::StreamSessionConnectInfo(Settings *settings, ChiakiTarget target, QString host, QByteArray regist_key, QByteArray morning, bool fullscreen) : settings(settings) { key_map = settings->GetControllerMappingForDecoding(); @@ -24,6 +24,7 @@ StreamSessionConnectInfo::StreamSessionConnectInfo(Settings *settings, QString h log_level_mask = settings->GetLogLevelMask(); log_file = CreateLogFilename(); video_profile = settings->GetVideoProfile(); + this->target = target; this->host = host; this->regist_key = regist_key; this->morning = morning; @@ -86,6 +87,7 @@ StreamSession::StreamSession(const StreamSessionConnectInfo &connect_info, QObje QByteArray host_str = connect_info.host.toUtf8(); ChiakiConnectInfo chiaki_connect_info; + chiaki_connect_info.ps5 = chiaki_target_is_ps5(connect_info.target); chiaki_connect_info.host = host_str.constData(); chiaki_connect_info.video_profile = connect_info.video_profile; diff --git a/lib/include/chiaki/common.h b/lib/include/chiaki/common.h index aa70cbc..8ad249b 100644 --- a/lib/include/chiaki/common.h +++ b/lib/include/chiaki/common.h @@ -61,7 +61,7 @@ CHIAKI_EXPORT void chiaki_aligned_free(void *ptr); typedef enum { // values must not change - CHIAKI_TARGET_PS4_UNKNOWN = 0, + CHIAKI_TARGET_PS4_UNKNOWN = 0, CHIAKI_TARGET_PS4_8 = 800, CHIAKI_TARGET_PS4_9 = 900, CHIAKI_TARGET_PS4_10 = 1000, @@ -82,6 +82,13 @@ static inline bool chiaki_target_is_ps5(ChiakiTarget target) { return target >= */ CHIAKI_EXPORT ChiakiErrorCode chiaki_lib_init(); +typedef enum +{ + CHIAKI_CODEC_H264 = 0, + CHIAKI_CODEC_H265, + CHIAKI_CODEC_H265_HDR +} ChiakiCodec; + #ifdef __cplusplus } #endif diff --git a/lib/include/chiaki/session.h b/lib/include/chiaki/session.h index 5da7087..e036cf7 100644 --- a/lib/include/chiaki/session.h +++ b/lib/include/chiaki/session.h @@ -48,6 +48,7 @@ typedef struct chiaki_connect_video_profile_t unsigned int height; unsigned int max_fps; unsigned int bitrate; + ChiakiCodec codec; } ChiakiConnectVideoProfile; typedef enum { diff --git a/lib/src/ctrl.c b/lib/src/ctrl.c index f953e05..b910fc9 100644 --- a/lib/src/ctrl.c +++ b/lib/src/ctrl.c @@ -813,6 +813,40 @@ static ChiakiErrorCode ctrl_connect(ChiakiCtrl *ctrl) goto error; } + char streaming_type_b64[256]; + bool have_streaming_type = chiaki_target_is_ps5(session->target); + if(have_streaming_type) + { + uint32_t streaming_type; + switch(session->connect_info.video_profile.codec) + { + case CHIAKI_CODEC_H265: + streaming_type = 2; + break; + case CHIAKI_CODEC_H265_HDR: + streaming_type = 3; + break; + default: + streaming_type = 1; + break; + } + uint8_t streaming_type_buf[4] = { + streaming_type & 0xff, + (streaming_type >> 8) & 0xff, + (streaming_type >> 0x10) & 0xff, + (streaming_type >> 0x18) & 0xff + }; + uint8_t streaming_type_enc[4] = { 0 }; + err = chiaki_rpcrypt_encrypt(&session->rpcrypt, ctrl->crypt_counter_local++, + streaming_type_buf, streaming_type_enc, 4); + if(err != CHIAKI_ERR_SUCCESS) + goto error; + + err = chiaki_base64_encode(streaming_type_enc, 4, streaming_type_b64, sizeof(streaming_type_b64)); + if(err != CHIAKI_ERR_SUCCESS) + goto error; + } + static const char request_fmt[] = "GET %s HTTP/1.1\r\n" "Host: %s:%d\r\n" @@ -827,11 +861,16 @@ static ChiakiErrorCode ctrl_connect(ChiakiCtrl *ctrl) "RP-OSType: %s\r\n" "RP-ConPath: 1\r\n" "%s%s%s" + "%s%s%s" "\r\n"; - const char *path = (session->target == CHIAKI_TARGET_PS4_8 || session->target == CHIAKI_TARGET_PS4_9) - ? "/sce/rp/session/ctrl" - : "/sie/ps4/rp/sess/ctrl"; + const char *path; + if(session->target == CHIAKI_TARGET_PS4_8 || session->target == CHIAKI_TARGET_PS4_9) + path = "/sce/rp/session/ctrl"; + else if(chiaki_target_is_ps5(session->target)) + path = "/sie/ps5/rp/sess/ctrl"; + else + path = "/sie/ps4/rp/sess/ctrl"; const char *rp_version = chiaki_rp_version_string(session->target); char buf[512]; @@ -840,7 +879,10 @@ static ChiakiErrorCode ctrl_connect(ChiakiCtrl *ctrl) rp_version ? rp_version : "", did_b64, ostype_b64, have_bitrate ? "RP-StartBitrate: " : "", have_bitrate ? bitrate_b64 : "", - have_bitrate ? "\r\n" : ""); + have_bitrate ? "\r\n" : "", + have_streaming_type ? "RP-StreamingType: " : "", + have_streaming_type ? streaming_type_b64 : "", + have_streaming_type ? "\r\n" : ""); if(request_len < 0 || request_len >= sizeof(buf)) goto error; diff --git a/lib/src/rpcrypt.c b/lib/src/rpcrypt.c index 781ecb8..197580e 100644 --- a/lib/src/rpcrypt.c +++ b/lib/src/rpcrypt.c @@ -12,7 +12,7 @@ #include #include - +#include static const uint8_t echo_b[] = { 0xe1, 0xec, 0x9c, 0x3a, 0xdd, 0xbd, 0x08, 0x85, 0xfc, 0x0e, 0x1d, 0x78, 0x90, 0x32, 0xc0, 0x04 }; @@ -40,9 +40,9 @@ static void bright_ambassador_ps4_pre10(uint8_t *bright, uint8_t *ambassador, co } } -static void bright_ambassador(uint8_t *bright, uint8_t *ambassador, const uint8_t *nonce, const uint8_t *morning) +static ChiakiErrorCode bright_ambassador(ChiakiTarget target, uint8_t *bright, uint8_t *ambassador, const uint8_t *nonce, const uint8_t *morning) { - static const uint8_t static_keys_a[0x70 * 0x20] = { + static const uint8_t keys_a_ps4[0x70 * 0x20] = { 0xdf, 0x40, 0x82, 0xa6, 0x2e, 0xd8, 0x6b, 0x2b, 0xf5, 0x6c, 0x5f, 0xf5, 0xfb, 0x21, 0x69, 0xb4, 0x00, 0x27, 0x5e, 0x87, 0xfa, 0x1a, 0xa7, 0x95, 0x16, 0xa1, 0xb7, 0xb7, 0xca, 0xf2, @@ -404,7 +404,369 @@ static void bright_ambassador(uint8_t *bright, uint8_t *ambassador, const uint8_ 0xed, 0xe4, 0x03, 0xd9 }; - static const uint8_t static_keys_b[0x70 * 0x20] = { + static const uint8_t keys_a_ps5[0x70 * 0x20] = { + 0x7f, 0xae, 0x83, 0x20, 0x47, 0x39, 0xb6, 0x5b, 0x23, 0x9b, + 0x5c, 0xa5, 0x37, 0xc4, 0xdf, 0x59, 0x26, 0x81, 0x0a, 0x53, + 0xa1, 0xae, 0xa3, 0xa3, 0xf9, 0xbe, 0xaa, 0x9f, 0xca, 0xe0, + 0x9e, 0x45, 0x8d, 0x43, 0x7f, 0x53, 0x3e, 0xc0, 0x17, 0x90, + 0xd2, 0x03, 0x2d, 0x92, 0xa8, 0x1b, 0x3e, 0x17, 0x9a, 0x9d, + 0xc9, 0x45, 0x39, 0x9e, 0x6c, 0x5f, 0xb6, 0xb2, 0x42, 0x57, + 0x6a, 0x1a, 0xa0, 0x35, 0x89, 0xdd, 0x6a, 0xc6, 0xa9, 0x2d, + 0x06, 0xa2, 0xd3, 0x70, 0xd5, 0xa7, 0x91, 0x32, 0x42, 0x5a, + 0x44, 0x0f, 0x35, 0x86, 0x18, 0xc0, 0x75, 0x9f, 0x8e, 0x07, + 0xb6, 0x80, 0x46, 0xc1, 0xbc, 0xe3, 0x4f, 0xc0, 0xdc, 0xa1, + 0xaf, 0xfb, 0x1b, 0x2b, 0x4d, 0xb6, 0x14, 0x8f, 0xaf, 0xeb, + 0x49, 0xdd, 0x2a, 0xb3, 0xfd, 0xb2, 0x32, 0x86, 0x3c, 0xe7, + 0x1a, 0x96, 0x14, 0x9d, 0xb1, 0xd9, 0x90, 0x72, 0xb3, 0xb3, + 0x3d, 0x6f, 0x3d, 0xcc, 0x26, 0xbc, 0x51, 0xda, 0x91, 0x6e, + 0xc6, 0xa9, 0xf9, 0x9f, 0x86, 0x07, 0x13, 0x70, 0xdc, 0xe2, + 0x21, 0xd7, 0x4c, 0xb3, 0xc3, 0x98, 0x02, 0x94, 0x9a, 0x63, + 0x6d, 0x34, 0x73, 0x2d, 0xa1, 0x23, 0x83, 0xa8, 0x58, 0x34, + 0x3f, 0x30, 0x1b, 0x9b, 0x01, 0xfb, 0xfa, 0xe4, 0x02, 0xa3, + 0x52, 0x4d, 0xfa, 0xc8, 0x16, 0xb8, 0xfa, 0xc5, 0x67, 0xe1, + 0x89, 0x28, 0xe1, 0xe5, 0xf4, 0xd7, 0xa2, 0x9f, 0xcc, 0xd4, + 0x39, 0x91, 0x41, 0xe9, 0xa2, 0x7a, 0x44, 0xb4, 0xb6, 0x4b, + 0xe9, 0xd7, 0x2c, 0x0b, 0x13, 0xc5, 0xd9, 0xa4, 0x50, 0x0c, + 0x15, 0x94, 0xc8, 0x65, 0x88, 0xac, 0xe4, 0x14, 0x77, 0x23, + 0x04, 0x9e, 0x30, 0xec, 0x3f, 0x27, 0xe7, 0xc2, 0xb1, 0x76, + 0x15, 0xbb, 0x45, 0x44, 0x6f, 0x1b, 0xcd, 0x2b, 0xae, 0x07, + 0x77, 0x63, 0x1a, 0xde, 0x10, 0xad, 0xe6, 0xde, 0x8c, 0x54, + 0xe5, 0x98, 0xde, 0xcc, 0x3b, 0x8b, 0xe1, 0x52, 0xb2, 0x78, + 0xa4, 0xa6, 0xa8, 0x43, 0x0a, 0xdb, 0x6a, 0xd6, 0x97, 0xe4, + 0x1a, 0x51, 0x8a, 0x81, 0x9c, 0x7d, 0xb9, 0x35, 0x67, 0x7a, + 0xfa, 0xbf, 0x59, 0xce, 0x6c, 0x30, 0xd8, 0x00, 0x95, 0x49, + 0x73, 0x75, 0xc6, 0xfc, 0xb7, 0xf4, 0x93, 0x16, 0x18, 0xc9, + 0x1b, 0x6e, 0x71, 0x84, 0x30, 0x85, 0xe3, 0x00, 0x19, 0x43, + 0xcb, 0x04, 0x61, 0x58, 0x83, 0x91, 0x8b, 0xf8, 0x0a, 0x3d, + 0x4c, 0x47, 0x71, 0x3c, 0x34, 0xf0, 0xa1, 0xee, 0x5d, 0xea, + 0xc0, 0xb9, 0x68, 0xee, 0xa2, 0x61, 0xb9, 0xb4, 0x69, 0x12, + 0x09, 0x90, 0x74, 0x4b, 0xc4, 0xbc, 0xaf, 0x32, 0x6b, 0x9c, + 0x69, 0x72, 0x15, 0x3b, 0xb2, 0x5d, 0x2c, 0x59, 0x95, 0x2b, + 0x72, 0xc3, 0xae, 0x45, 0x4c, 0xda, 0xfa, 0x0c, 0x66, 0x9c, + 0xf8, 0x23, 0xf3, 0x74, 0x9d, 0xf9, 0x0e, 0x5c, 0x18, 0x22, + 0x02, 0x0f, 0x96, 0x0c, 0xe9, 0x9b, 0xb9, 0x05, 0xf5, 0xf9, + 0x38, 0xb7, 0xcd, 0x36, 0x03, 0xb0, 0x89, 0x48, 0x3e, 0x34, + 0xb5, 0xe6, 0x8f, 0xf0, 0xa7, 0x20, 0xad, 0x42, 0x0f, 0x55, + 0xbc, 0x16, 0x8c, 0x81, 0xab, 0xb7, 0x6d, 0x8b, 0x88, 0x88, + 0xde, 0x14, 0x59, 0x7b, 0xf4, 0x28, 0x8a, 0x58, 0x26, 0xbc, + 0xed, 0x03, 0x27, 0x7a, 0x59, 0x35, 0xdd, 0x0f, 0xff, 0x23, + 0xbb, 0x18, 0x93, 0x56, 0xff, 0x48, 0xd8, 0xd3, 0x71, 0xa1, + 0xc3, 0x61, 0x30, 0x83, 0xd4, 0x06, 0xcf, 0x41, 0xde, 0x81, + 0x7a, 0x10, 0x16, 0xce, 0xe7, 0x9f, 0xdc, 0x97, 0x24, 0xc3, + 0x62, 0x04, 0x42, 0xbb, 0xb7, 0xad, 0x13, 0x4e, 0x2b, 0x0c, + 0xf1, 0x36, 0x53, 0xb5, 0xa4, 0x14, 0xec, 0xdf, 0x8a, 0xa1, + 0x9e, 0xd3, 0x62, 0xd1, 0xe8, 0xc1, 0xa3, 0x9a, 0x87, 0x76, + 0x06, 0x0a, 0x84, 0x76, 0x7b, 0x88, 0xdc, 0xeb, 0x4a, 0x68, + 0xa4, 0x0f, 0x83, 0x62, 0xfd, 0xf5, 0x01, 0x50, 0x8b, 0xbc, + 0x6c, 0xd0, 0xe9, 0x60, 0x2c, 0xe5, 0xef, 0x88, 0xce, 0xc6, + 0x83, 0x4f, 0x7a, 0x32, 0xa3, 0xc0, 0x0e, 0x05, 0x83, 0x26, + 0x49, 0x3b, 0x88, 0xe5, 0xb9, 0x26, 0xc5, 0x8f, 0xbb, 0xe3, + 0x54, 0x05, 0x6b, 0x8d, 0x42, 0x56, 0x04, 0x01, 0xd8, 0x0b, + 0xe7, 0x83, 0x0a, 0x66, 0xb2, 0x94, 0xf1, 0x8c, 0x92, 0xb2, + 0xc3, 0xa4, 0x77, 0x1c, 0xb1, 0x5d, 0x24, 0x43, 0x70, 0x2e, + 0x1d, 0x43, 0xa5, 0x03, 0x27, 0x94, 0xf6, 0xaa, 0x06, 0xba, + 0xa0, 0x58, 0xf0, 0xc5, 0xc8, 0xe9, 0x80, 0x09, 0x98, 0xc9, + 0xc1, 0x95, 0xd2, 0x13, 0x33, 0xc8, 0xfb, 0x03, 0x5a, 0x37, + 0xa4, 0xdc, 0x44, 0xd7, 0x7e, 0x4b, 0x41, 0xcb, 0xb0, 0xac, + 0xda, 0x78, 0x80, 0x80, 0x6c, 0xb2, 0xdf, 0xe6, 0x5c, 0x4c, + 0x1e, 0x82, 0xd6, 0x76, 0x56, 0x21, 0xef, 0xd0, 0x6c, 0x0e, + 0xe1, 0x6a, 0xbb, 0x07, 0xfc, 0xbd, 0xa2, 0x34, 0x86, 0x5f, + 0xb0, 0x98, 0xb9, 0xa4, 0xc9, 0x85, 0xd4, 0xb0, 0xb1, 0x45, + 0xfc, 0x3c, 0x52, 0x17, 0x28, 0x5c, 0xe5, 0x10, 0xc7, 0x77, + 0x03, 0xba, 0x51, 0x89, 0x73, 0x6a, 0x28, 0x68, 0xb4, 0x01, + 0xb2, 0xf3, 0x6d, 0x8f, 0x41, 0xf2, 0x88, 0x19, 0x1a, 0x2a, + 0x86, 0xe2, 0xd6, 0xb5, 0x40, 0xbc, 0xf7, 0xd8, 0x6b, 0xe9, + 0xe2, 0x4f, 0x03, 0xae, 0x14, 0x7e, 0x5c, 0xfd, 0xa1, 0xf6, + 0xda, 0xa7, 0x6c, 0xeb, 0xe4, 0x01, 0xa0, 0x64, 0x9e, 0xb6, + 0xad, 0x31, 0xf6, 0xba, 0x1d, 0x88, 0x85, 0xfd, 0x62, 0x6c, + 0x4b, 0x10, 0xc3, 0x3d, 0x97, 0x10, 0xfa, 0x2f, 0x83, 0x23, + 0x55, 0xb0, 0x7d, 0xe6, 0x20, 0xe9, 0xae, 0x89, 0x10, 0xe4, + 0xfc, 0xfd, 0x82, 0xe4, 0x2b, 0x5a, 0x4e, 0xea, 0x17, 0xca, + 0xf8, 0x05, 0xba, 0x03, 0x9f, 0x06, 0x02, 0x3d, 0xb4, 0x9c, + 0x35, 0x27, 0xab, 0x47, 0xb1, 0x67, 0x78, 0x79, 0x4c, 0x6e, + 0xb1, 0x97, 0xb3, 0xb7, 0x7f, 0x54, 0xa7, 0x02, 0x55, 0x35, + 0x4d, 0x0f, 0x62, 0x64, 0x9b, 0xe6, 0x7b, 0xbb, 0x65, 0x5d, + 0x53, 0x21, 0x00, 0x7c, 0xe3, 0x9b, 0xf8, 0x68, 0xae, 0xbc, + 0x0d, 0x5f, 0xdc, 0xed, 0x4d, 0xfe, 0xd0, 0x2e, 0x6c, 0x93, + 0x0c, 0xef, 0x3d, 0x98, 0x74, 0x7b, 0x30, 0xdb, 0x6c, 0xa1, + 0x73, 0x20, 0x08, 0xf8, 0x79, 0xf9, 0x04, 0xe4, 0xa3, 0xaa, + 0xf8, 0x84, 0x50, 0x47, 0x9f, 0x05, 0x34, 0xb6, 0x18, 0x05, + 0xc5, 0x93, 0xb4, 0xb6, 0x07, 0x54, 0xbf, 0xff, 0x6b, 0x1f, + 0x4b, 0xd6, 0x5f, 0x90, 0x69, 0x5e, 0x12, 0xd9, 0x84, 0x48, + 0xf3, 0xb8, 0xe2, 0x95, 0x97, 0xc0, 0xaf, 0x4d, 0x68, 0x88, + 0x1d, 0xa2, 0xa1, 0x96, 0x0a, 0x32, 0xe7, 0x56, 0x6b, 0xc3, + 0x3c, 0xee, 0x89, 0x31, 0xd5, 0xf1, 0x23, 0x5c, 0xec, 0x45, + 0x3c, 0x55, 0x8f, 0xa6, 0x39, 0x08, 0xa8, 0xf2, 0x01, 0x28, + 0xd4, 0x97, 0x7f, 0x91, 0x63, 0x49, 0xe2, 0xc7, 0xf8, 0x7e, + 0xcb, 0x01, 0x1a, 0xdd, 0xe0, 0x57, 0xf8, 0x7f, 0x71, 0x3b, + 0x17, 0xd4, 0xd9, 0x57, 0x20, 0xed, 0x49, 0x4d, 0x8f, 0x57, + 0xc5, 0xaa, 0x65, 0x70, 0x9f, 0xf1, 0x2c, 0x74, 0x0d, 0x57, + 0x07, 0x16, 0x26, 0x74, 0x8e, 0x7a, 0x5e, 0x4f, 0x44, 0xab, + 0x77, 0xe9, 0x79, 0xe2, 0x98, 0xe5, 0xca, 0x64, 0x45, 0x7c, + 0xbf, 0x8c, 0x7c, 0x0d, 0xba, 0x71, 0xec, 0xbf, 0x92, 0x46, + 0x96, 0x75, 0x5e, 0x61, 0xe2, 0x7c, 0x49, 0x4f, 0xcf, 0xa1, + 0x9a, 0xc4, 0x3b, 0x99, 0x0b, 0x8a, 0xd2, 0x12, 0x33, 0x17, + 0x20, 0xa0, 0x76, 0x17, 0x23, 0x3f, 0x76, 0xac, 0xb4, 0xcc, + 0x07, 0x5a, 0x62, 0x22, 0x0b, 0x21, 0x27, 0xf6, 0x80, 0x3d, + 0x30, 0xdc, 0x8c, 0x2e, 0xf7, 0xf3, 0x96, 0xc0, 0xbf, 0x03, + 0x10, 0xd6, 0xbe, 0xa3, 0xe4, 0x19, 0x21, 0x28, 0x49, 0xe4, + 0xa7, 0xe6, 0x4e, 0x69, 0x79, 0x8f, 0x09, 0x55, 0x77, 0x5d, + 0x90, 0x5f, 0xfa, 0x83, 0x35, 0xc8, 0xab, 0xe3, 0xd8, 0x18, + 0x7c, 0x14, 0x8a, 0x73, 0x68, 0x37, 0xc2, 0xf2, 0xb6, 0x89, + 0xc0, 0x4b, 0xbf, 0x5c, 0xb3, 0x9e, 0x50, 0x84, 0x9a, 0x95, + 0xbf, 0x14, 0x95, 0x61, 0x9f, 0xb9, 0x40, 0xa9, 0x9c, 0xb2, + 0xf1, 0x3d, 0x1b, 0x34, 0xc9, 0x26, 0xbf, 0xb3, 0x0d, 0x2f, + 0x85, 0x6c, 0x48, 0xc0, 0x66, 0x56, 0xc3, 0x1a, 0x30, 0xea, + 0x58, 0x96, 0x15, 0xc9, 0xf8, 0x19, 0x7b, 0xa0, 0x07, 0xb3, + 0x01, 0xc8, 0xfc, 0xe0, 0xb6, 0xf4, 0x01, 0x7e, 0x58, 0x7e, + 0x08, 0x09, 0x48, 0x33, 0x86, 0x9a, 0xea, 0xb4, 0x48, 0x51, + 0xe8, 0x5e, 0x69, 0x20, 0x6a, 0x38, 0x77, 0x1b, 0x8a, 0xa5, + 0x43, 0xe4, 0x67, 0x14, 0x96, 0x18, 0xc4, 0x7e, 0x4b, 0xbd, + 0x90, 0xd6, 0x03, 0x43, 0xce, 0xdc, 0x85, 0x23, 0x68, 0x90, + 0x09, 0x72, 0xc9, 0x8d, 0x4d, 0x89, 0xe2, 0x48, 0x6d, 0x03, + 0x2f, 0x42, 0xe0, 0x1a, 0x5b, 0x96, 0x15, 0x16, 0x3a, 0x56, + 0xa9, 0xa9, 0xb1, 0xac, 0xf3, 0x96, 0x58, 0x6d, 0x61, 0x13, + 0x92, 0x79, 0xf1, 0x58, 0x0b, 0x56, 0x5b, 0x2f, 0xe5, 0x43, + 0x35, 0x82, 0x78, 0xce, 0x3c, 0x71, 0x6c, 0x91, 0xc7, 0xc9, + 0xd2, 0xbf, 0x30, 0xb6, 0x68, 0x87, 0x50, 0x06, 0x98, 0x54, + 0x51, 0x2b, 0x2e, 0x57, 0x15, 0x01, 0x1f, 0x85, 0x06, 0x5f, + 0xf0, 0xbe, 0x22, 0x09, 0x14, 0x2a, 0xbf, 0x34, 0x11, 0x9e, + 0x31, 0xa8, 0x7c, 0x9e, 0x17, 0x03, 0x3d, 0x0c, 0xe8, 0x21, + 0x0b, 0x8b, 0x61, 0x03, 0x33, 0x2e, 0xc2, 0x06, 0xc0, 0xc6, + 0x0c, 0xa4, 0xf1, 0x79, 0x18, 0x02, 0xde, 0xcf, 0x3d, 0x6c, + 0xf7, 0x5d, 0x3c, 0x81, 0x81, 0x38, 0x1a, 0x63, 0x90, 0x47, + 0xaa, 0x73, 0x25, 0xd9, 0x18, 0x3b, 0x88, 0xcd, 0xde, 0x06, + 0xd6, 0xbe, 0xcf, 0x2e, 0x20, 0x6a, 0x32, 0xbb, 0xf0, 0x12, + 0xfd, 0x3e, 0xa1, 0xbf, 0x58, 0x63, 0x8d, 0x0c, 0xaf, 0x1f, + 0x81, 0xc2, 0xb7, 0x03, 0xc2, 0xf7, 0x54, 0x81, 0x5b, 0x04, + 0x6e, 0x3e, 0x49, 0x5a, 0xba, 0xfa, 0x39, 0xec, 0xcf, 0xe8, + 0x12, 0x88, 0x30, 0x83, 0xa6, 0x2a, 0xd2, 0x08, 0xd5, 0xae, + 0x3c, 0x63, 0xb4, 0xd2, 0xd9, 0x09, 0x00, 0x47, 0x2a, 0xa3, + 0xae, 0xa6, 0x4e, 0x9e, 0xab, 0x65, 0x40, 0x32, 0xa4, 0x49, + 0xc7, 0xcc, 0x28, 0x0c, 0xe6, 0x60, 0xf8, 0x23, 0xd4, 0x8b, + 0xde, 0xd9, 0xbf, 0x10, 0x18, 0xf4, 0x39, 0x9f, 0x7e, 0x64, + 0x9e, 0xd4, 0x8c, 0x04, 0xe6, 0xec, 0xc6, 0x09, 0xc8, 0x2c, + 0xa4, 0xd4, 0xe2, 0x4b, 0x84, 0x1b, 0xc4, 0x04, 0x90, 0xdf, + 0xf4, 0x4c, 0x68, 0xe8, 0x32, 0xf6, 0xcc, 0xa0, 0x51, 0x48, + 0x92, 0x6c, 0x01, 0x9e, 0xb2, 0x24, 0x0b, 0x76, 0x76, 0x7d, + 0x2a, 0xb4, 0x68, 0x26, 0xe3, 0xae, 0x0d, 0xe5, 0x33, 0xf8, + 0x19, 0x86, 0x19, 0x4f, 0x5a, 0x14, 0xfb, 0x2f, 0x13, 0x83, + 0xce, 0x0a, 0x24, 0xcb, 0x42, 0x7a, 0xa6, 0x26, 0x6d, 0x52, + 0xd6, 0x9b, 0xab, 0x15, 0x36, 0x08, 0xbf, 0x75, 0x41, 0x16, + 0x75, 0x46, 0xb2, 0x59, 0xe0, 0xd2, 0x58, 0xf1, 0xc0, 0x25, + 0xf1, 0x11, 0x4a, 0x6c, 0x39, 0x3c, 0xce, 0xda, 0xec, 0x94, + 0xdc, 0xcb, 0x0a, 0xef, 0x5a, 0x5f, 0x68, 0x13, 0xc7, 0x75, + 0x0f, 0xdd, 0x7d, 0x80, 0x21, 0x6b, 0x32, 0xdc, 0x83, 0x8a, + 0xa4, 0xab, 0x1c, 0xc7, 0x03, 0x29, 0x3e, 0xee, 0xf3, 0x8b, + 0xc8, 0x5a, 0x9a, 0x30, 0x61, 0xc8, 0x1d, 0x51, 0xa8, 0x8b, + 0xd7, 0xa1, 0xf0, 0x6f, 0xe3, 0xec, 0x88, 0x11, 0x3b, 0x9d, + 0xbf, 0x64, 0xda, 0xed, 0x3e, 0x6d, 0xc6, 0xea, 0x36, 0xe1, + 0xbc, 0x54, 0x41, 0x2e, 0x4b, 0x38, 0xa5, 0x29, 0xab, 0xca, + 0x48, 0x53, 0x9e, 0x31, 0x05, 0x5d, 0x50, 0x78, 0xe5, 0x0d, + 0x2d, 0x87, 0x7c, 0x83, 0xf1, 0xe7, 0x18, 0xb2, 0x87, 0xf7, + 0x7f, 0x7f, 0x29, 0xd9, 0xe0, 0x1f, 0xc8, 0xa3, 0xc3, 0x3c, + 0x29, 0x7e, 0x02, 0x5d, 0x3e, 0x0c, 0xa3, 0xa2, 0x90, 0x6b, + 0xe9, 0x19, 0x57, 0x0a, 0x25, 0x6a, 0x19, 0xef, 0x5c, 0x38, + 0xab, 0x44, 0xed, 0x04, 0xaa, 0x58, 0x8e, 0xb7, 0xf6, 0x41, + 0x52, 0x52, 0x42, 0xc8, 0xe3, 0x3f, 0x3a, 0x07, 0xca, 0xa5, + 0xdb, 0x22, 0x83, 0x29, 0xe1, 0x99, 0xa1, 0x89, 0x16, 0x27, + 0xb2, 0x4e, 0x19, 0x66, 0xd8, 0xdd, 0x98, 0x70, 0xab, 0x83, + 0x11, 0x4e, 0xe8, 0x2d, 0x3d, 0x9c, 0x58, 0x8f, 0xd0, 0xeb, + 0xf7, 0x9a, 0xba, 0x1c, 0x16, 0xd0, 0xcd, 0xa8, 0xff, 0xdb, + 0x3f, 0xbc, 0x19, 0xae, 0x90, 0x1b, 0xce, 0x02, 0x1d, 0x75, + 0xd3, 0xd1, 0x6f, 0xd1, 0xc2, 0xf0, 0xeb, 0xab, 0xb1, 0xe2, + 0x47, 0xc1, 0x4b, 0x73, 0x46, 0x7b, 0xbc, 0x3b, 0xef, 0xfb, + 0xe0, 0x63, 0xcf, 0xaa, 0xcf, 0x4c, 0x06, 0x91, 0xd4, 0x00, + 0xbd, 0x87, 0x20, 0x0d, 0x4e, 0x2d, 0x09, 0x7c, 0x13, 0xb4, + 0xbb, 0x6f, 0x21, 0x6a, 0x30, 0xa8, 0x4d, 0x91, 0x46, 0xc0, + 0x4b, 0xfb, 0x6e, 0x74, 0xf6, 0x4d, 0xbb, 0x32, 0x5b, 0xf1, + 0x27, 0x4b, 0xa3, 0x6a, 0x1d, 0xf8, 0x1e, 0x9a, 0xc0, 0x6c, + 0x16, 0x04, 0x4b, 0xca, 0xb6, 0xd4, 0xae, 0x79, 0xa9, 0x6c, + 0x3c, 0x2e, 0xb2, 0xb4, 0xef, 0x73, 0xd2, 0xe9, 0xf4, 0x71, + 0x2f, 0x34, 0xe3, 0x7c, 0x1b, 0xf1, 0x52, 0x0d, 0xa8, 0x5f, + 0xac, 0x7c, 0x48, 0xf0, 0xb3, 0x37, 0x05, 0x4d, 0xbe, 0x69, + 0x27, 0xda, 0x04, 0xc2, 0x33, 0xa3, 0x45, 0xb1, 0x3d, 0x3f, + 0xe1, 0x12, 0x94, 0x2e, 0xd6, 0xf1, 0xfa, 0xaf, 0x1b, 0x91, + 0xe3, 0x3f, 0x59, 0x40, 0x25, 0xd0, 0xba, 0xda, 0xa0, 0x46, + 0x14, 0x59, 0x1d, 0x29, 0xd3, 0xdc, 0xfb, 0xbe, 0x25, 0xb5, + 0x1d, 0x9d, 0xd5, 0x93, 0x03, 0x8f, 0xb1, 0x72, 0x45, 0xf7, + 0x6b, 0x89, 0xbb, 0x04, 0xbf, 0x2c, 0x17, 0x7a, 0x2a, 0x6c, + 0xa1, 0x85, 0xac, 0x88, 0x90, 0x8a, 0xee, 0x7e, 0x25, 0x39, + 0x29, 0xe8, 0x6a, 0x8f, 0x68, 0x24, 0x97, 0xb0, 0x4d, 0x8e, + 0x9c, 0x9b, 0xb5, 0xd4, 0x0b, 0x6c, 0xd4, 0x14, 0xb5, 0xa1, + 0x47, 0x30, 0x1d, 0xd8, 0x84, 0xf6, 0xb5, 0xba, 0xb6, 0xf2, + 0x90, 0xce, 0xd6, 0x9c, 0x71, 0x71, 0xe0, 0xf3, 0xcc, 0x8b, + 0xa6, 0x18, 0x88, 0x6d, 0x1e, 0x2f, 0x3c, 0x6b, 0x69, 0x89, + 0xe1, 0x33, 0xc5, 0xfb, 0x4c, 0x53, 0x55, 0x55, 0x10, 0xb7, + 0xcf, 0x35, 0x84, 0xf6, 0x07, 0xd2, 0xae, 0xc2, 0xc4, 0xf5, + 0x9c, 0xd8, 0xeb, 0x7d, 0xf7, 0xdc, 0xb4, 0xbb, 0xf0, 0xbe, + 0xdd, 0x3d, 0x3f, 0xcb, 0xd2, 0xa1, 0x52, 0x9e, 0xc9, 0x5c, + 0x05, 0x03, 0xf9, 0x9c, 0xf6, 0xdf, 0xb3, 0xd4, 0xe2, 0x54, + 0x34, 0x62, 0x66, 0xf8, 0xa6, 0xed, 0x6d, 0xef, 0x9f, 0xa6, + 0x49, 0x61, 0x87, 0x2f, 0x54, 0x48, 0x99, 0xdd, 0xd5, 0xa3, + 0xe2, 0xc7, 0xbb, 0x69, 0x17, 0x3a, 0xdc, 0x6f, 0xbd, 0xb6, + 0x84, 0xf3, 0xa2, 0x8c, 0x3f, 0x16, 0x12, 0x50, 0x62, 0xa0, + 0xc8, 0x47, 0xd3, 0xa6, 0x9a, 0x67, 0xe6, 0xc5, 0xf9, 0x00, + 0xca, 0x32, 0x9f, 0x20, 0x87, 0xd7, 0xea, 0x15, 0x18, 0xbb, + 0xe0, 0x24, 0x4f, 0x5b, 0x02, 0xb4, 0x2e, 0xc9, 0x1c, 0x66, + 0x45, 0x54, 0x01, 0xe3, 0xe0, 0xd0, 0xc2, 0x5c, 0x8c, 0x69, + 0x1c, 0xa7, 0xf7, 0x15, 0x25, 0x7d, 0x76, 0xb4, 0xba, 0x8d, + 0xa0, 0x5f, 0x1e, 0x46, 0x8e, 0xe1, 0x40, 0xd2, 0xb7, 0x1f, + 0x2f, 0x78, 0xec, 0xb5, 0x81, 0xc4, 0xa7, 0xb1, 0x07, 0x4d, + 0x7f, 0xa1, 0x15, 0x85, 0x7b, 0xdb, 0xaa, 0x84, 0x80, 0xc6, + 0x5a, 0x32, 0xcb, 0xa3, 0x0f, 0x5f, 0xb9, 0x71, 0xae, 0xfa, + 0x03, 0x2a, 0xb5, 0xf8, 0x61, 0x96, 0x80, 0xcb, 0xdf, 0x69, + 0xc6, 0xb0, 0x05, 0x4d, 0x1a, 0x72, 0x6b, 0xb0, 0xed, 0x3e, + 0x52, 0x6f, 0x60, 0xf5, 0x64, 0x4e, 0x18, 0xbb, 0x45, 0x0c, + 0xe7, 0xc2, 0x1a, 0xbc, 0xcc, 0xff, 0x6d, 0x3f, 0x15, 0x57, + 0x94, 0x1c, 0xa2, 0x25, 0x3d, 0xb5, 0x46, 0xe6, 0xc1, 0x26, + 0x39, 0xe1, 0x3d, 0xa4, 0x44, 0x74, 0xf3, 0x0e, 0x92, 0x3c, + 0xd6, 0xa1, 0x93, 0xe0, 0xd6, 0x7b, 0x5d, 0xd3, 0x60, 0x60, + 0x6a, 0xa4, 0xa0, 0x20, 0x02, 0x6e, 0x89, 0x54, 0xf7, 0x00, + 0x8a, 0xc1, 0x3c, 0x77, 0xfe, 0x60, 0xb6, 0xa0, 0x12, 0x1e, + 0xd1, 0xfd, 0x9f, 0x3d, 0xc6, 0x16, 0x1f, 0x09, 0x01, 0x97, + 0x10, 0x3a, 0xa1, 0x59, 0xa4, 0xf8, 0xde, 0xe3, 0x15, 0xfe, + 0x95, 0x83, 0x0f, 0x9c, 0xc2, 0xcb, 0xe6, 0x91, 0x3d, 0x49, + 0x36, 0xc9, 0xfc, 0x02, 0x40, 0x1f, 0x3a, 0x91, 0xf9, 0x6e, + 0x08, 0xa7, 0xb3, 0x8b, 0x9d, 0x6c, 0x7e, 0x71, 0xab, 0xe6, + 0x61, 0xa2, 0xb1, 0xef, 0x31, 0xa8, 0xa9, 0x02, 0x1a, 0xa2, + 0x89, 0xf8, 0xec, 0xee, 0x97, 0xf6, 0xa3, 0xc0, 0xa7, 0x7f, + 0xca, 0xb9, 0x75, 0x52, 0x18, 0xf3, 0x8d, 0x02, 0xb8, 0x59, + 0x7a, 0x20, 0xf6, 0xf3, 0x14, 0x33, 0x68, 0x70, 0xfc, 0x02, + 0xe1, 0x74, 0x4f, 0x6e, 0xc9, 0x9c, 0x5d, 0xc7, 0xc0, 0xb3, + 0xe4, 0x5b, 0x68, 0x5d, 0x56, 0x41, 0x8d, 0x88, 0x40, 0x96, + 0x55, 0xfe, 0x71, 0x90, 0x50, 0x61, 0x34, 0xc1, 0x60, 0xa9, + 0xf0, 0x92, 0x07, 0x70, 0x72, 0x71, 0xfc, 0x53, 0x35, 0x9c, + 0x25, 0x35, 0xc9, 0x01, 0xde, 0xa0, 0x6e, 0x1e, 0x3e, 0x05, + 0x95, 0x57, 0xa3, 0xb5, 0xbe, 0xc0, 0xc5, 0x9e, 0x4c, 0xd0, + 0x09, 0x50, 0xfa, 0xd3, 0x1b, 0x6b, 0x90, 0xce, 0x85, 0x48, + 0x12, 0xbe, 0x9d, 0x95, 0xf3, 0xc7, 0x5d, 0xcb, 0x12, 0x08, + 0x2c, 0x49, 0xe1, 0xd5, 0x93, 0x67, 0x52, 0xa3, 0xac, 0x8f, + 0xdc, 0x8f, 0x64, 0xaa, 0x0c, 0x58, 0x52, 0x30, 0x4e, 0xef, + 0xf8, 0xfe, 0x2b, 0xbe, 0xd3, 0x6c, 0x7b, 0x30, 0xd6, 0xff, + 0x42, 0x94, 0xdc, 0xbf, 0x4e, 0x6b, 0x0d, 0xef, 0x5c, 0xd9, + 0x84, 0x43, 0x10, 0x22, 0x85, 0x24, 0xe9, 0x9a, 0x4b, 0xaf, + 0xb6, 0x9c, 0xce, 0x7b, 0x59, 0x0f, 0x3c, 0x51, 0x16, 0xc8, + 0x2b, 0x18, 0xd9, 0xce, 0x9f, 0x82, 0x86, 0xef, 0xc8, 0xaa, + 0x56, 0xfa, 0xbd, 0x0f, 0xd0, 0x8f, 0x66, 0x76, 0xed, 0x66, + 0x48, 0x8b, 0xa4, 0x67, 0xd1, 0xca, 0xd2, 0x62, 0x13, 0x7c, + 0xde, 0xdf, 0xfe, 0xfc, 0xf9, 0x1a, 0x16, 0x4a, 0xfc, 0xb2, + 0xc1, 0xdd, 0x92, 0x44, 0x9e, 0xe5, 0x44, 0x62, 0xee, 0xeb, + 0xbe, 0xa4, 0x9a, 0x95, 0x3d, 0xb3, 0x69, 0x3f, 0xda, 0x9a, + 0x24, 0xdc, 0x90, 0x56, 0xe0, 0x46, 0x3e, 0x90, 0xf5, 0xea, + 0x98, 0x57, 0x89, 0x9b, 0xf1, 0xd2, 0x67, 0xd7, 0x46, 0x43, + 0xf5, 0x7d, 0x7d, 0x2c, 0xe1, 0x6a, 0xd2, 0x79, 0x77, 0xef, + 0xd1, 0xa1, 0x0f, 0x8c, 0x1a, 0xd7, 0x19, 0x55, 0x86, 0xf2, + 0xc7, 0xa8, 0x9b, 0x32, 0xf9, 0x5e, 0x45, 0x23, 0x52, 0x2d, + 0xb6, 0xdc, 0x65, 0xbf, 0x98, 0xa4, 0x41, 0xff, 0xbb, 0x89, + 0x79, 0xec, 0x16, 0xa2, 0xf8, 0x05, 0x69, 0x64, 0xdf, 0x95, + 0x6b, 0x7d, 0xfc, 0xce, 0x3d, 0xa1, 0xd8, 0xbf, 0x83, 0xe2, + 0x89, 0xc9, 0x1a, 0x36, 0xb9, 0xff, 0xc3, 0x6f, 0x04, 0x1a, + 0xfc, 0x5c, 0x6c, 0x3f, 0x78, 0x7c, 0xf3, 0xe2, 0x0b, 0x72, + 0x55, 0x13, 0x8b, 0x97, 0x12, 0x58, 0xf4, 0x4a, 0x52, 0x36, + 0xc6, 0x42, 0xca, 0xf5, 0xf0, 0x31, 0x9d, 0xf3, 0x60, 0x56, + 0x26, 0xff, 0x81, 0xf8, 0xd3, 0x82, 0x76, 0x1d, 0xb8, 0x9e, + 0xc8, 0x53, 0xab, 0x54, 0x22, 0xf8, 0xef, 0x37, 0x8e, 0xc3, + 0x7b, 0xe9, 0xbf, 0x25, 0x2e, 0x56, 0x2a, 0x18, 0xa9, 0xec, + 0x39, 0x28, 0xa5, 0x9c, 0x8e, 0xc5, 0xaa, 0x7d, 0xa6, 0x97, + 0x8f, 0x44, 0xcf, 0x62, 0xb3, 0xa1, 0x6f, 0x26, 0xcf, 0x5c, + 0x3d, 0x5c, 0x47, 0x40, 0x8f, 0x5d, 0x70, 0x68, 0x01, 0x69, + 0x26, 0x5d, 0x45, 0xa0, 0x60, 0xb1, 0x49, 0xa9, 0xe3, 0x6c, + 0xe7, 0x2a, 0x64, 0x02, 0x9e, 0x43, 0x37, 0x0f, 0x57, 0xd3, + 0xa5, 0xeb, 0x11, 0xe6, 0x69, 0x55, 0xd1, 0x85, 0x6c, 0x78, + 0xc9, 0x8b, 0x44, 0xbc, 0x85, 0x80, 0x93, 0xba, 0x6c, 0xe3, + 0xd2, 0x39, 0x95, 0xc6, 0x49, 0xd1, 0xa9, 0x46, 0x50, 0xd6, + 0xb2, 0x7c, 0x12, 0x5c, 0xa7, 0x7f, 0x2c, 0xb3, 0xaa, 0x01, + 0x4a, 0x06, 0x58, 0xef, 0xe3, 0xad, 0x8c, 0xdc, 0xc6, 0x45, + 0xe6, 0x18, 0x33, 0xcb, 0x19, 0xba, 0x8f, 0x76, 0x72, 0x41, + 0xd5, 0xe3, 0xa9, 0x8f, 0x93, 0x50, 0xdc, 0x7a, 0xcd, 0x4e, + 0x94, 0xc2, 0xaa, 0xb4, 0xc0, 0xf4, 0xad, 0xec, 0x46, 0xff, + 0x65, 0x73, 0x54, 0xb5, 0xfc, 0xbb, 0xd2, 0x61, 0x14, 0xae, + 0xab, 0xf7, 0x52, 0x95, 0xcd, 0x1c, 0x3f, 0x25, 0xc9, 0xc9, + 0xe3, 0x19, 0x79, 0x62, 0xac, 0x02, 0x85, 0x8a, 0xc9, 0x3a, + 0xf1, 0xc5, 0xd2, 0xbd, 0xdc, 0x8f, 0xd7, 0xce, 0xfe, 0xa2, + 0x58, 0x14, 0xc5, 0xa2, 0xd9, 0xef, 0x2d, 0x9d, 0x97, 0xf7, + 0xec, 0x55, 0xf7, 0x2e, 0x4c, 0x0f, 0x2f, 0x3b, 0xde, 0xc0, + 0x41, 0x7f, 0x7d, 0x2a, 0xb9, 0x56, 0xab, 0xe6, 0xf4, 0x11, + 0x52, 0xb6, 0xe6, 0xc9, 0xe6, 0xc6, 0xef, 0x02, 0x6e, 0x4a, + 0xd1, 0x6d, 0x3c, 0xa6, 0x0c, 0xb8, 0xd6, 0x7e, 0x31, 0x3a, + 0xa0, 0xce, 0xcc, 0x0c, 0x31, 0xab, 0xc8, 0xe8, 0x37, 0xfd, + 0x0e, 0x5b, 0xc3, 0x62, 0xf8, 0xdb, 0xcb, 0xf5, 0x00, 0xa0, + 0x40, 0xb3, 0x48, 0xeb, 0x82, 0xae, 0xd2, 0xd3, 0x4a, 0xea, + 0x12, 0x6f, 0x39, 0x85, 0xbb, 0x98, 0xb7, 0x39, 0x3f, 0xf3, + 0x0b, 0x0e, 0x6d, 0xca, 0x84, 0xf8, 0xf1, 0x35, 0xdc, 0xe7, + 0xbd, 0x1c, 0xdd, 0x21, 0x4b, 0xdf, 0xde, 0xb5, 0xcc, 0x16, + 0x87, 0x0f, 0xe8, 0xb8, 0x34, 0x86, 0x45, 0xfc, 0xf4, 0x61, + 0x93, 0x4a, 0x7f, 0x2c, 0x3f, 0xeb, 0xd1, 0xce, 0xc8, 0x61, + 0x0b, 0x72, 0x49, 0x3a, 0xd3, 0x3f, 0x06, 0x05, 0x4c, 0xe0, + 0x23, 0x4d, 0x8a, 0x20, 0xae, 0x87, 0xda, 0x4c, 0x6c, 0xb8, + 0xb9, 0x79, 0xda, 0x1c, 0x73, 0x5e, 0xb6, 0xf2, 0x03, 0x61, + 0x68, 0xff, 0xd5, 0xbf, 0x9a, 0x04, 0xb4, 0x73, 0xc8, 0xda, + 0xb5, 0x82, 0x34, 0x0c, 0x95, 0xfb, 0xda, 0x70, 0xcb, 0xb2, + 0xde, 0xa8, 0x08, 0xdf, 0xe4, 0x24, 0xff, 0x77, 0x5c, 0x2f, + 0x18, 0x0c, 0x82, 0x1f, 0xe6, 0x29, 0x09, 0x8f, 0xfe, 0x67, + 0x96, 0xb2, 0x47, 0x9c, 0x43, 0x3a, 0x58, 0xe8, 0x5f, 0xc9, + 0x73, 0xa4, 0x65, 0x3c, 0xe3, 0x7f, 0xdc, 0xde, 0x0d, 0xdf, + 0x1c, 0x4d, 0x0e, 0x3e, 0xc3, 0xd2, 0x1c, 0xc1, 0x0e, 0x4a, + 0x44, 0xf7, 0x74, 0xce, 0x00, 0x69, 0x43, 0x99, 0xbd, 0xb7, + 0xa1, 0x36, 0xff, 0x0a, 0xc8, 0x6b, 0xdd, 0xd8, 0x62, 0x2f, + 0xf5, 0x37, 0x95, 0x8d, 0x91, 0xd8, 0xa0, 0x5b, 0xc9, 0xa7, + 0xfe, 0x6c, 0xc0, 0x45, 0x7c, 0x00, 0x0d, 0x4a, 0xbf, 0x3e, + 0x00, 0x50, 0xe6, 0xce, 0x69, 0x26, 0xa6, 0x1d, 0xa4, 0xb0, + 0x4b, 0x4e, 0x02, 0x5b, 0x01, 0x0a, 0x1c, 0xcb, 0x11, 0x6e, + 0x74, 0xfe, 0xcc, 0xe1, 0x62, 0xd7, 0xf1, 0x56, 0xf9, 0x3b, + 0x69, 0x46, 0x8c, 0x1c, 0x52, 0x37, 0x25, 0x33, 0x35, 0x03, + 0xea, 0x51, 0x7a, 0x29, 0x85, 0x9a, 0x58, 0x4f, 0x38, 0x9f, + 0xc6, 0x57, 0x54, 0xba, 0x8e, 0x92, 0x59, 0xcc, 0x4f, 0x88, + 0xd5, 0x4a, 0x33, 0x82, 0x9f, 0x2c, 0x7d, 0x2b, 0x99, 0xce, + 0x22, 0x3b, 0x2d, 0xc9, 0x6f, 0x4d, 0x38, 0x87, 0x45, 0x20, + 0xc5, 0x67, 0x41, 0xf2, 0xce, 0xd9, 0x3b, 0x77, 0x41, 0xad, + 0x35, 0x93, 0x0a, 0x53, 0x1a, 0xb2, 0x15, 0xc9, 0x91, 0xb9, + 0xc7, 0x68, 0x5c, 0x1b, 0x29, 0xd2, 0x99, 0xb5, 0x38, 0xa2, + 0x60, 0x5d, 0xc2, 0x2c, 0x36, 0xef, 0xf7, 0xed, 0xfa, 0xe2, + 0x33, 0xdb, 0xb4, 0x35, 0x23, 0xd4, 0x18, 0x90, 0x4e, 0xe3, + 0xb4, 0xec, 0xcd, 0x42, 0xbf, 0x35, 0x28, 0x24, 0x88, 0x18, + 0x3d, 0xfc, 0x97, 0xa6, 0x12, 0x96, 0x68, 0x5b, 0xd4, 0x5b, + 0xf0, 0xe9, 0xda, 0xc7, 0xbd, 0xe1, 0xca, 0x3f, 0x86, 0xff, + 0x8e, 0x16, 0xaa, 0x28, 0x5a, 0x9d, 0xbd, 0x6a, 0xd3, 0xa7, + 0x7b, 0xf8, 0x32, 0x39, 0x87, 0xa2, 0x21, 0x03, 0x11, 0x13, + 0xe6, 0xe7, 0x86, 0x68, 0x92, 0x93, 0x0a, 0x71, 0xa5, 0x5b, + 0x95, 0x01, 0xae, 0x09, 0xb5, 0x68, 0xee, 0x0d, 0xd7, 0x2f, + 0x24, 0x7a, 0x44, 0xf4, 0x00, 0x58, 0x23, 0x07, 0xd3, 0x25, + 0x7c, 0x1a, 0x48, 0x4a, 0x20, 0x06, 0x1d, 0x1f, 0xc4, 0xf8, + 0x5b, 0xa1, 0x88, 0xb3, 0x53, 0x2b, 0x65, 0x89, 0xd4, 0x26, + 0x0b, 0xc1, 0xc4, 0x0f, 0xc5, 0x2f, 0xab, 0xf6, 0xc8, 0xc6, + 0x98, 0x73, 0x81, 0x91, 0x87, 0x7c, 0x9f, 0x50, 0x66, 0x50, + 0xc3, 0xcc, 0xff, 0x7a, 0x9c, 0x25, 0x14, 0x73, 0x94, 0x4b, + 0x8a, 0x51, 0x98, 0x23, 0x52, 0x96, 0x20, 0xa8, 0xe9, 0x0f, + 0x38, 0x36, 0xc4, 0x51, 0xec, 0xea, 0xe4, 0xea, 0xe9, 0x8e, + 0x44, 0x47, 0x13, 0x75, 0xea, 0xb1, 0x90, 0x0a, 0x66, 0xd1, + 0x17, 0xd5, 0xd7, 0x2f, 0x9e, 0x56, 0x86, 0xd1, 0x98, 0x9c, + 0x24, 0xa2, 0x82, 0x84, 0xad, 0xee, 0x50, 0xd8, 0xc1, 0x52, + 0xf1, 0xa7, 0xa2, 0x96, 0xfe, 0x34, 0x6f, 0xf2, 0xf7, 0x37, + 0x53, 0x7f, 0xa5, 0x4f, 0x4a, 0x5e, 0xe5, 0x65, 0x2c, 0xb1, + 0xc5, 0x22, 0x7b, 0xbe, 0xb0, 0x99, 0x61, 0xc8, 0xf3, 0x54, + 0x20, 0x9c, 0x6e, 0x45, 0x1a, 0xdf, 0x68, 0x5c, 0x80, 0x32, + 0xad, 0xd3, 0xa4, 0xac, 0x28, 0xeb, 0x02, 0xc8, 0x31, 0xc0, + 0x74, 0x5b, 0x9b, 0xe0, 0x20, 0x72, 0x12, 0x93, 0xc2, 0x93, + 0x2b, 0x77, 0xcb, 0x2c, 0x08, 0xaa, 0x48, 0x07, 0x05, 0x77, + 0x27, 0x6d, 0x43, 0x1b, 0x75, 0xc5, 0xda, 0x1d, 0x6b, 0x41, + 0x6b, 0x56, 0x5c, 0xf3, 0x0e, 0x8b, 0x17, 0xa2, 0xe8, 0x15, + 0x63, 0xbd, 0x07, 0xfa, 0xcf, 0xea, 0x1b, 0xe7, 0x6d, 0xc1, + 0xf3, 0x8a, 0x0b, 0x21, 0x4e, 0x66, 0x47, 0xa4, 0xd4, 0xed, + 0x3b, 0xfa, 0x7c, 0x65, 0xac, 0x4d, 0x38, 0xda, 0x4f, 0x2a, + 0xc9, 0x52, 0xb1, 0x6e, 0x1f, 0xae, 0xba, 0xde, 0x8c, 0x1d, + 0x73, 0x49, 0xdc, 0x7b, 0x04, 0xdf, 0xda, 0xdc, 0x24, 0xd6, + 0xe1, 0x0a, 0x1a, 0x30, 0x85, 0x00, 0xea, 0xe1, 0xc8, 0xa9, + 0x98, 0x84, 0xdd, 0xa6, 0x8b, 0xe6, 0xbf, 0x96, 0x2b, 0x9f, + 0x27, 0xce, 0x71, 0x4d, 0x4b, 0xdc, 0x21, 0xcb, 0x2f, 0x17, + 0xa8, 0xa5, 0x98, 0xa3, 0x7a, 0xb1, 0x24, 0x79, 0x92, 0x1d, + 0xbd, 0x8d, 0x22, 0x8d, 0xb8, 0x1a, 0x5e, 0x0c, 0x3f, 0x00, + 0xe7, 0xe6, 0x9b, 0x43, 0xcc, 0xe1, 0x9f, 0x1c, 0xde, 0x17, + 0x59, 0xfd, 0xf4, 0xfc + }; + + static const uint8_t keys_b_ps4[0x70 * 0x20] = { 0x76, 0x65, 0x9d, 0xe4, 0xf7, 0x6c, 0x45, 0x1f, 0x73, 0x42, 0x47, 0xc3, 0x92, 0x32, 0xf4, 0x38, 0x6f, 0x28, 0xcf, 0xfa, 0x41, 0x84, 0xbe, 0x1b, 0x5a, 0x23, 0x99, 0xfa, 0xcd, 0x74, @@ -766,25 +1128,417 @@ static void bright_ambassador(uint8_t *bright, uint8_t *ambassador, const uint8_ 0xdc, 0x32, 0xeb, 0x20 }; - const uint8_t *key = &static_keys_a[(nonce[0] >> 3) * 0x70]; + static const uint8_t keys_b_ps5[0x70 * 0x20] = { + 0xe1, 0x34, 0x17, 0x1d, 0xf5, 0x17, 0xda, 0xe1, 0x1b, 0xe6, + 0x0c, 0x73, 0x35, 0xd4, 0x6b, 0x42, 0x28, 0xcd, 0x47, 0x06, + 0x4e, 0x96, 0x87, 0xd5, 0x9d, 0x09, 0xe7, 0x24, 0xa0, 0x1b, + 0xad, 0x32, 0x6d, 0x06, 0x75, 0xdf, 0x77, 0x69, 0x89, 0xeb, + 0x03, 0x24, 0xd6, 0x31, 0x23, 0xc0, 0xf6, 0x0d, 0x60, 0x0c, + 0xb6, 0xad, 0x4b, 0xba, 0x9e, 0xb2, 0x66, 0xfd, 0x8d, 0xb5, + 0xdd, 0xc9, 0xf7, 0x67, 0xc3, 0x93, 0x3f, 0x1e, 0xc1, 0x87, + 0x5c, 0x34, 0xae, 0x24, 0x44, 0xc0, 0x34, 0xf3, 0xd9, 0x1e, + 0x3b, 0x62, 0x0b, 0xa0, 0x00, 0xa2, 0x1e, 0x13, 0x9b, 0xa6, + 0xe8, 0x8b, 0x91, 0xb1, 0x42, 0xb1, 0x2b, 0xa7, 0xce, 0xc6, + 0x85, 0x6e, 0xe9, 0x22, 0xfc, 0x6d, 0x77, 0xb2, 0x9b, 0x71, + 0x5d, 0x1a, 0xf4, 0xb6, 0x93, 0x72, 0xae, 0x46, 0x40, 0x5d, + 0x26, 0x30, 0x98, 0xf8, 0xc7, 0xbe, 0xb7, 0x2c, 0x6e, 0x1b, + 0x13, 0x32, 0x7a, 0xb2, 0x81, 0xa6, 0x96, 0xc4, 0xe8, 0x15, + 0x82, 0xc4, 0xea, 0xc3, 0x28, 0x70, 0xaf, 0x3f, 0xf6, 0x7f, + 0x13, 0xa6, 0x73, 0x27, 0x29, 0xdf, 0x17, 0x38, 0xd4, 0xb0, + 0x72, 0xed, 0x91, 0xb5, 0xb1, 0xe6, 0xd7, 0x3a, 0x34, 0xa9, + 0x67, 0x45, 0x67, 0x02, 0xc2, 0xc6, 0x3c, 0x22, 0xf0, 0x22, + 0xec, 0xff, 0x7d, 0xda, 0x2c, 0xe4, 0x67, 0x47, 0x6b, 0x31, + 0x63, 0xf4, 0xb3, 0x5d, 0xdb, 0x47, 0x33, 0xa6, 0x1c, 0x21, + 0xa3, 0x48, 0x0f, 0x7f, 0xc9, 0x84, 0x1a, 0x57, 0x0d, 0xd5, + 0x72, 0xd6, 0x20, 0x34, 0x36, 0x41, 0x0e, 0x43, 0x11, 0xe5, + 0x43, 0x91, 0x05, 0x69, 0xcf, 0x79, 0x70, 0x90, 0xc5, 0x7a, + 0xf3, 0xb1, 0x3d, 0x59, 0x08, 0x59, 0xe1, 0x41, 0xae, 0x65, + 0x2e, 0x5c, 0xcb, 0x04, 0x8d, 0x4c, 0xac, 0x50, 0x21, 0x93, + 0x06, 0x36, 0xb6, 0xe0, 0xb1, 0xdc, 0x4c, 0x98, 0x48, 0x8f, + 0x95, 0x12, 0x64, 0x40, 0x87, 0x6b, 0xdd, 0xf7, 0x79, 0x0a, + 0xc1, 0x1d, 0x67, 0xf9, 0xf5, 0xf8, 0xce, 0x75, 0x94, 0x0a, + 0x66, 0x17, 0xec, 0x17, 0xee, 0xfa, 0xf6, 0x88, 0x8b, 0xeb, + 0x2e, 0x06, 0xf1, 0x77, 0x1d, 0x22, 0xd3, 0x4c, 0x4f, 0x8d, + 0xa8, 0x6a, 0x53, 0xc7, 0xd4, 0x72, 0xb4, 0xec, 0x97, 0x9d, + 0x31, 0x48, 0xed, 0x62, 0xd2, 0x40, 0x94, 0x23, 0x4c, 0x10, + 0x37, 0x20, 0x3b, 0xb6, 0x9f, 0x1c, 0x99, 0xcf, 0xce, 0x9d, + 0xda, 0xe7, 0xc9, 0x32, 0x88, 0xe9, 0x1d, 0x37, 0xb5, 0xa7, + 0x8b, 0x19, 0x8a, 0x50, 0x50, 0x2d, 0xa9, 0x89, 0x4e, 0x6e, + 0x7d, 0xbf, 0xd2, 0x7e, 0xe1, 0xa2, 0x6b, 0x3e, 0xf1, 0x33, + 0x0e, 0x74, 0x88, 0x96, 0xd8, 0xa3, 0x00, 0x2d, 0x9b, 0xaf, + 0x71, 0xac, 0x68, 0x63, 0x2f, 0xa9, 0x46, 0x6c, 0x73, 0x5d, + 0xb0, 0xd6, 0x92, 0xa3, 0x03, 0xc4, 0x93, 0xb6, 0x48, 0x7f, + 0x3c, 0x15, 0x1b, 0xb1, 0xe0, 0x40, 0x56, 0x8f, 0x63, 0xa6, + 0x94, 0xc9, 0x8b, 0xde, 0x66, 0x2a, 0x5f, 0x32, 0x4a, 0x8d, + 0xa6, 0x50, 0xdf, 0xdf, 0xaa, 0x20, 0xe1, 0xeb, 0x71, 0x05, + 0xd7, 0xca, 0xe1, 0xa8, 0x79, 0xbc, 0x83, 0x94, 0x74, 0x85, + 0x58, 0x89, 0xc8, 0x59, 0x97, 0xaa, 0xf0, 0xfa, 0x5a, 0x37, + 0xa7, 0x4a, 0x13, 0xe6, 0x36, 0x0f, 0xe6, 0x45, 0x29, 0x89, + 0xb6, 0x11, 0x32, 0xdb, 0x0d, 0x75, 0x58, 0xe2, 0x9a, 0x1a, + 0x1b, 0x08, 0x66, 0x2a, 0x3d, 0x66, 0xe1, 0x24, 0x82, 0xe6, + 0x0b, 0xc5, 0xe6, 0xd7, 0x9c, 0x18, 0xa3, 0x4d, 0x2f, 0x4e, + 0x46, 0xd6, 0x21, 0x76, 0xaf, 0xad, 0xa9, 0xd8, 0x37, 0xd1, + 0x3d, 0xfe, 0x32, 0x24, 0xdc, 0xc4, 0xbe, 0x69, 0x2e, 0x97, + 0x8f, 0x09, 0xb1, 0x55, 0x9b, 0x3d, 0xe2, 0x46, 0x1c, 0x99, + 0x27, 0x4d, 0x06, 0xff, 0x72, 0x7a, 0xef, 0xe9, 0x9a, 0xc4, + 0xe9, 0x93, 0xc4, 0xf0, 0xb6, 0x64, 0xa9, 0xea, 0xf2, 0x09, + 0xd6, 0xa3, 0x20, 0x6c, 0xd1, 0x1b, 0xd7, 0x7f, 0x74, 0x00, + 0xf5, 0x60, 0xf8, 0xf4, 0x99, 0x93, 0xa5, 0xc2, 0x2b, 0xb1, + 0xe9, 0x83, 0x39, 0x33, 0x4d, 0x16, 0x0b, 0xb8, 0xa6, 0x6d, + 0x30, 0x9b, 0x97, 0xbe, 0x9f, 0x0c, 0x1e, 0x4e, 0xff, 0x3a, + 0xe2, 0x06, 0x15, 0xba, 0x57, 0x9d, 0x60, 0x76, 0x4d, 0x50, + 0x88, 0x6d, 0x8b, 0x9d, 0xf9, 0x70, 0x3d, 0x66, 0x87, 0x11, + 0x1e, 0x9a, 0x24, 0xaa, 0xa5, 0xe8, 0xf8, 0x38, 0x6c, 0x3e, + 0x6d, 0xda, 0xe5, 0xfc, 0xde, 0x9e, 0x2a, 0xe4, 0xe6, 0xee, + 0x07, 0xce, 0x48, 0x5e, 0xd8, 0x27, 0x23, 0x10, 0xc9, 0x72, + 0x9d, 0x7c, 0xd9, 0x5d, 0xa8, 0xba, 0x8a, 0x1e, 0x22, 0xef, + 0x46, 0x22, 0x6f, 0x79, 0x2e, 0x6f, 0x73, 0xbf, 0xcd, 0xcf, + 0x4f, 0x13, 0xdc, 0x5d, 0x51, 0x0b, 0xd5, 0xa9, 0x76, 0xc5, + 0xe3, 0xc0, 0x90, 0xe9, 0x32, 0x28, 0xfc, 0x9f, 0xf1, 0x88, + 0x1f, 0xa6, 0xb3, 0x45, 0x04, 0xa7, 0x1c, 0xbd, 0x66, 0xe7, + 0xbb, 0xc1, 0x49, 0x42, 0xdb, 0x52, 0x4b, 0xbb, 0xd9, 0x63, + 0x8f, 0x6d, 0x9d, 0x3b, 0xd7, 0xe3, 0x49, 0xbc, 0x7c, 0xd9, + 0xdb, 0x50, 0x67, 0x6f, 0x07, 0x6d, 0x08, 0x2a, 0xea, 0x02, + 0xeb, 0xbe, 0x22, 0xd1, 0x17, 0x1b, 0xfa, 0x39, 0x1c, 0x6c, + 0xda, 0x56, 0x37, 0xd3, 0x8e, 0xf4, 0xe3, 0x22, 0xaa, 0x6c, + 0xc1, 0x75, 0x71, 0x4c, 0x21, 0x36, 0xfb, 0xe0, 0x02, 0x54, + 0x6a, 0x48, 0x31, 0x40, 0x7b, 0x64, 0x7c, 0x76, 0x31, 0xe5, + 0x0f, 0xc1, 0xd8, 0xb4, 0x29, 0xd9, 0x9c, 0x0c, 0x80, 0xce, + 0xa3, 0x7c, 0x2f, 0x0f, 0x4c, 0x11, 0xb5, 0x3d, 0x85, 0x93, + 0xbd, 0x60, 0xd4, 0x5c, 0x3f, 0x41, 0x54, 0xe6, 0x50, 0xb1, + 0xb1, 0xed, 0x85, 0x19, 0x30, 0x81, 0x6a, 0x95, 0xfe, 0x4a, + 0x93, 0x28, 0x12, 0x3d, 0xdc, 0x76, 0xac, 0x59, 0xe6, 0xe3, + 0x56, 0x81, 0x1d, 0x29, 0xf9, 0x65, 0x48, 0xc3, 0x70, 0x50, + 0x2f, 0x2c, 0x4c, 0x69, 0x01, 0xdc, 0x75, 0x3d, 0x14, 0xff, + 0x5f, 0xaf, 0x6a, 0xc7, 0xc1, 0xa3, 0x3b, 0x12, 0xa5, 0x6d, + 0xd7, 0x95, 0xdd, 0x3d, 0x70, 0x67, 0xcb, 0x2f, 0x22, 0x5d, + 0xac, 0x9d, 0xf2, 0x1c, 0x6a, 0x27, 0x45, 0x59, 0x29, 0x5a, + 0x4f, 0x6c, 0x34, 0x43, 0x43, 0x94, 0xd1, 0xe6, 0x1d, 0xde, + 0xda, 0x7c, 0x4f, 0xdf, 0xf6, 0xa1, 0x42, 0xde, 0x88, 0xc7, + 0xcc, 0xbb, 0x99, 0xb0, 0x49, 0xa5, 0x9d, 0x6a, 0xae, 0x3f, + 0xb0, 0x59, 0x2c, 0xfa, 0xb8, 0x66, 0x26, 0xf5, 0xdb, 0xb7, + 0xe8, 0xc5, 0xb3, 0x45, 0xfa, 0xd2, 0xd3, 0xec, 0x1b, 0xcf, + 0x83, 0xc5, 0x96, 0x66, 0x92, 0x27, 0xd4, 0x88, 0xbf, 0x3a, + 0x7e, 0x00, 0x22, 0xb2, 0x8a, 0x80, 0xfe, 0xf9, 0x75, 0x1a, + 0xd1, 0xcc, 0xaf, 0x97, 0x9f, 0x6c, 0xd6, 0x7f, 0x1f, 0x6e, + 0xe1, 0x5f, 0x77, 0x2b, 0x96, 0xd6, 0x21, 0x73, 0xf9, 0x6a, + 0x8e, 0x99, 0x65, 0x19, 0x0f, 0x91, 0xe7, 0x51, 0x57, 0x2c, + 0x59, 0x7b, 0xdc, 0xb2, 0x84, 0x06, 0x12, 0x6d, 0xf9, 0x61, + 0x4c, 0xa7, 0x4f, 0x2c, 0x8a, 0x58, 0xfb, 0xc3, 0x6c, 0x90, + 0x9b, 0xc3, 0x19, 0x72, 0x34, 0x44, 0x9b, 0x2f, 0x53, 0xc3, + 0x6f, 0x4e, 0xf7, 0xff, 0x7f, 0x22, 0x04, 0xd4, 0x5c, 0xd7, + 0x18, 0x07, 0x29, 0xc8, 0x3e, 0x2b, 0xbe, 0xa9, 0xba, 0x17, + 0x3b, 0xcb, 0xa8, 0xb0, 0x63, 0x90, 0x09, 0x22, 0x4d, 0xbf, + 0x54, 0x31, 0x4f, 0x41, 0x8b, 0xaa, 0x8b, 0x8e, 0x19, 0x07, + 0x60, 0x62, 0xdc, 0x4b, 0x99, 0x64, 0x95, 0xea, 0xf6, 0xcc, + 0xfc, 0x97, 0xe5, 0x7f, 0xd4, 0xd9, 0xb9, 0x9b, 0x5a, 0x28, + 0x05, 0x8f, 0x30, 0x7d, 0x5f, 0x11, 0x55, 0x62, 0x64, 0x98, + 0x9c, 0x2a, 0x6b, 0x6e, 0x64, 0xac, 0x3f, 0x1f, 0xbc, 0x45, + 0x98, 0x0e, 0xed, 0x51, 0x92, 0x19, 0x84, 0x11, 0xb8, 0x8f, + 0xe7, 0xb7, 0x7a, 0x9b, 0xc0, 0x41, 0x50, 0x85, 0xc9, 0xc2, + 0x0e, 0xe9, 0x84, 0xb5, 0xcc, 0x72, 0xa6, 0xec, 0xd4, 0xdd, + 0x6f, 0x2f, 0xd0, 0xb6, 0xa6, 0x03, 0xaa, 0xd3, 0x99, 0x1c, + 0xb7, 0x5f, 0x7a, 0xda, 0x9f, 0xe7, 0xff, 0xa5, 0x8e, 0x77, + 0x4c, 0x14, 0x85, 0xd0, 0x78, 0x3c, 0x0f, 0xaf, 0x1c, 0x5c, + 0x09, 0x79, 0x6c, 0xb4, 0x64, 0xa1, 0x86, 0xd0, 0xb2, 0xfa, + 0xeb, 0xfc, 0xda, 0xf3, 0x42, 0x3a, 0x43, 0xb0, 0xd2, 0x24, + 0xfe, 0x04, 0xd1, 0x31, 0x47, 0x77, 0xde, 0x0f, 0xb4, 0x55, + 0x28, 0x83, 0x14, 0x5e, 0xd5, 0x4d, 0x99, 0xce, 0xf1, 0xb9, + 0x48, 0x6e, 0xf8, 0x17, 0xd5, 0x0a, 0xa0, 0xa3, 0x7a, 0xce, + 0x9a, 0x1a, 0x29, 0x83, 0xfc, 0x42, 0x58, 0x73, 0x48, 0x2c, + 0x18, 0xdd, 0x46, 0xb6, 0x0f, 0x5e, 0xbe, 0xa3, 0x3e, 0x2e, + 0xa0, 0xd6, 0xef, 0xe8, 0xf8, 0x88, 0xd8, 0x04, 0x44, 0x53, + 0x09, 0x33, 0x07, 0xfc, 0x70, 0x05, 0xc7, 0x4e, 0x56, 0xea, + 0x27, 0xbe, 0x15, 0x6c, 0x0a, 0xa7, 0x04, 0x5a, 0x35, 0x36, + 0x1c, 0x9e, 0xeb, 0x88, 0xee, 0xdd, 0x63, 0x4c, 0x34, 0xa9, + 0x70, 0x93, 0xc8, 0xea, 0x83, 0x79, 0x6f, 0x0e, 0x25, 0xc5, + 0x0b, 0x32, 0x55, 0xbc, 0x0a, 0x75, 0x90, 0x82, 0xa2, 0x18, + 0x2f, 0x12, 0x26, 0x31, 0x92, 0xeb, 0x47, 0x38, 0x60, 0x62, + 0xe9, 0xce, 0xb0, 0x42, 0x88, 0xe6, 0x84, 0x3c, 0x13, 0x37, + 0x4d, 0x2b, 0x74, 0x01, 0xef, 0x07, 0xb6, 0x9e, 0x99, 0x2a, + 0x84, 0x4b, 0x6f, 0x41, 0x9a, 0x8b, 0x0e, 0x01, 0xbb, 0x51, + 0xf9, 0x73, 0x7b, 0x7a, 0x51, 0x50, 0x68, 0x8a, 0x41, 0xbe, + 0xb6, 0x91, 0xf6, 0x5d, 0xe9, 0x66, 0xef, 0xfd, 0xf3, 0x8e, + 0xe2, 0x24, 0xcb, 0xca, 0x5b, 0xc5, 0x75, 0x87, 0x3f, 0x15, + 0x6e, 0xd3, 0xc0, 0x6f, 0xe2, 0x61, 0xab, 0x97, 0x7d, 0x22, + 0x67, 0xef, 0x3b, 0x84, 0xb3, 0xe7, 0x6c, 0x99, 0x77, 0x2f, + 0xed, 0xa8, 0xd1, 0xf6, 0x60, 0x38, 0x85, 0x1a, 0xef, 0x91, + 0x7d, 0x40, 0xa9, 0x3a, 0x6e, 0x45, 0xe0, 0x99, 0x83, 0x80, + 0x74, 0x02, 0xe3, 0x04, 0xe6, 0x7a, 0x64, 0xc9, 0x08, 0x8a, + 0xa4, 0x9e, 0x64, 0x01, 0xce, 0xa1, 0xb8, 0x3b, 0xf6, 0x22, + 0x0d, 0x8c, 0x7b, 0xf8, 0xfc, 0xd8, 0xf0, 0x92, 0xdc, 0x95, + 0xdd, 0xa8, 0x05, 0xde, 0x59, 0x2f, 0xc2, 0x17, 0x8d, 0xfa, + 0x6f, 0x7b, 0x2d, 0x07, 0x4d, 0x53, 0x3e, 0x80, 0x6c, 0x5d, + 0x62, 0xe4, 0xe1, 0x9f, 0x93, 0x8a, 0x2e, 0x59, 0xde, 0xad, + 0x44, 0xec, 0x47, 0x12, 0x68, 0xfd, 0x6f, 0x87, 0x14, 0xd5, + 0x0c, 0xe1, 0x93, 0xec, 0xc0, 0xa1, 0x93, 0x4d, 0x2d, 0xff, + 0x27, 0x00, 0x18, 0xe2, 0xb3, 0x40, 0xdf, 0x1d, 0x56, 0x05, + 0xb6, 0x42, 0x3c, 0x76, 0xde, 0xd2, 0x0d, 0xa6, 0xaf, 0x8b, + 0x71, 0x93, 0x7d, 0x92, 0x3f, 0x55, 0xf0, 0x99, 0x34, 0x26, + 0xa2, 0xa5, 0xc8, 0x15, 0xe9, 0x91, 0x6f, 0xd7, 0xc1, 0xc7, + 0xe6, 0xc5, 0xa1, 0xba, 0x08, 0x8e, 0x78, 0xac, 0x3b, 0xd2, + 0xd3, 0xc4, 0x46, 0x99, 0x97, 0x7d, 0x71, 0x27, 0xc9, 0x2f, + 0x15, 0x7c, 0x65, 0xc2, 0x2b, 0x40, 0xc8, 0x6c, 0x05, 0x8b, + 0x6f, 0x1e, 0xfc, 0x1f, 0x78, 0xd8, 0x48, 0x9a, 0x2f, 0xb4, + 0x38, 0xb7, 0xfc, 0x92, 0xdb, 0x97, 0x4d, 0x03, 0x4c, 0xf9, + 0xba, 0x69, 0x3f, 0x41, 0xbb, 0x39, 0xba, 0x3e, 0x74, 0xac, + 0x8e, 0x57, 0x8d, 0x45, 0x6b, 0x9d, 0x8a, 0xa1, 0x16, 0xb0, + 0xea, 0x7d, 0x12, 0xe5, 0x08, 0x0d, 0xc2, 0xba, 0x4f, 0x52, + 0xdb, 0xb8, 0xb6, 0x4c, 0x07, 0xb2, 0xdc, 0xbc, 0xc7, 0x77, + 0x01, 0xb0, 0xda, 0xfd, 0x15, 0x2b, 0x5e, 0xce, 0x5d, 0x2c, + 0x6b, 0x55, 0xbb, 0x5b, 0x44, 0xd1, 0xf8, 0x36, 0x9b, 0x81, + 0x19, 0x3a, 0x47, 0x18, 0x93, 0x23, 0x40, 0xaa, 0x14, 0x7f, + 0xe3, 0xb5, 0xd6, 0x1e, 0x70, 0x26, 0x85, 0xb3, 0x51, 0x27, + 0x1c, 0x45, 0xca, 0xb7, 0x47, 0xa9, 0x98, 0xf3, 0xb3, 0xa9, + 0x37, 0x33, 0xbf, 0x10, 0x82, 0x95, 0xce, 0xa9, 0xa4, 0x57, + 0x9c, 0xec, 0xde, 0xfb, 0x2b, 0x2b, 0xd8, 0xc7, 0x6d, 0xe4, + 0x4c, 0x29, 0x7e, 0x6e, 0xfb, 0x26, 0xe6, 0x42, 0x17, 0xdf, + 0xd0, 0x1f, 0xc2, 0xb7, 0x71, 0xd8, 0x44, 0x6b, 0xf6, 0x00, + 0x78, 0x51, 0xa0, 0x57, 0x28, 0x75, 0x00, 0x0e, 0x4f, 0xac, + 0x3d, 0xe6, 0x38, 0x75, 0xa0, 0x1a, 0xcf, 0x42, 0x20, 0xa0, + 0x22, 0x83, 0x80, 0xb3, 0xf6, 0x5f, 0x15, 0x85, 0xb5, 0x8d, + 0xd7, 0xae, 0xf2, 0xb8, 0xb1, 0xc1, 0xb7, 0xd6, 0xe8, 0x9f, + 0x6a, 0x68, 0x31, 0x7d, 0xbe, 0xf3, 0x75, 0xaa, 0x24, 0xc3, + 0x41, 0xa7, 0xfc, 0xaf, 0xd3, 0xd1, 0x71, 0xb0, 0x60, 0xcc, + 0x35, 0xbe, 0x09, 0x49, 0x7f, 0xf6, 0x97, 0x0b, 0xf1, 0x6b, + 0xe5, 0x2d, 0xec, 0x83, 0x53, 0x8a, 0x20, 0xb9, 0x96, 0xbb, + 0x61, 0x1e, 0x47, 0x32, 0x02, 0x2a, 0xe5, 0x1c, 0xa8, 0x9f, + 0xec, 0xbe, 0x2b, 0x46, 0xa6, 0x14, 0x9b, 0x84, 0xed, 0x92, + 0x90, 0xee, 0x08, 0x65, 0x4b, 0x06, 0x99, 0x23, 0x84, 0xba, + 0x97, 0xc1, 0x8c, 0x42, 0xde, 0x2e, 0x95, 0xe4, 0x9d, 0xe0, + 0x6f, 0x8d, 0x15, 0x89, 0xa9, 0x8b, 0x74, 0x2a, 0xf9, 0x4b, + 0xc7, 0x76, 0xd4, 0xf5, 0x53, 0xae, 0x95, 0x44, 0x4f, 0x61, + 0x86, 0x12, 0x9f, 0xf2, 0xe2, 0x37, 0x41, 0x09, 0x86, 0x52, + 0x3f, 0xac, 0xa4, 0x98, 0x4e, 0x42, 0x6c, 0x81, 0xc0, 0x82, + 0x82, 0xc2, 0x4c, 0xca, 0x80, 0x77, 0xd1, 0x08, 0x3b, 0x90, + 0x63, 0x97, 0x8c, 0x80, 0xab, 0x48, 0xba, 0x1b, 0x59, 0x77, + 0x37, 0x78, 0xb0, 0x95, 0xd2, 0x5b, 0xab, 0xd8, 0xf6, 0xf1, + 0xcf, 0x40, 0x1a, 0x6e, 0x40, 0x61, 0x95, 0x39, 0x2a, 0x17, + 0x58, 0x51, 0xda, 0xeb, 0x4c, 0x08, 0xf1, 0x94, 0xed, 0x44, + 0x48, 0xbb, 0xaa, 0xdd, 0x78, 0x76, 0x30, 0x31, 0x86, 0xfd, + 0x91, 0xd7, 0xf8, 0xd9, 0xd4, 0x1a, 0x5a, 0x92, 0x3f, 0x16, + 0x11, 0x48, 0xf8, 0x39, 0x79, 0x63, 0xa2, 0x00, 0x94, 0x13, + 0x70, 0xe8, 0xc1, 0x7e, 0x93, 0xf7, 0x39, 0x58, 0xee, 0x3b, + 0xfa, 0x81, 0xe6, 0xc3, 0x64, 0x31, 0x6b, 0xfd, 0xf8, 0x01, + 0x75, 0x14, 0xac, 0xb5, 0x18, 0x2f, 0xef, 0xd5, 0x42, 0x4d, + 0xa1, 0xa5, 0x46, 0x92, 0x5b, 0x37, 0x3d, 0x3f, 0x99, 0xd2, + 0x23, 0xd8, 0x75, 0x10, 0x81, 0x76, 0x91, 0xdb, 0x3d, 0xf3, + 0x19, 0xbf, 0xfc, 0xfc, 0xb1, 0x6c, 0x3b, 0x6a, 0x67, 0xf0, + 0xb9, 0x0a, 0x6e, 0xc2, 0xc6, 0x12, 0x88, 0x54, 0x78, 0x66, + 0x68, 0xb6, 0xe7, 0x81, 0x39, 0x6f, 0x18, 0x45, 0x34, 0x4c, + 0x99, 0xaa, 0x4d, 0x23, 0xf8, 0x5f, 0xcb, 0xe7, 0xec, 0xbe, + 0xbe, 0xf3, 0x37, 0x93, 0x96, 0x7d, 0x4f, 0x57, 0x95, 0xf5, + 0xea, 0x6a, 0xca, 0xd1, 0x4e, 0x8a, 0xf0, 0x46, 0x34, 0xc5, + 0x79, 0x78, 0xce, 0x79, 0x47, 0x9e, 0xb7, 0x92, 0x51, 0x2d, + 0x8f, 0x5d, 0xab, 0x9a, 0x5e, 0x4a, 0x2d, 0x39, 0x2c, 0x11, + 0x24, 0x22, 0x0f, 0xac, 0x65, 0xbf, 0xd6, 0xda, 0xef, 0x82, + 0x00, 0xfb, 0x40, 0x53, 0x55, 0xf7, 0xe5, 0xa5, 0x6a, 0x84, + 0xc6, 0x36, 0x1a, 0x72, 0xa6, 0x2a, 0xe0, 0xb2, 0xa3, 0xfe, + 0x1f, 0x32, 0x9c, 0xff, 0x8a, 0x3c, 0xcc, 0xd1, 0xf0, 0x8c, + 0x99, 0xbe, 0xd1, 0x82, 0xb6, 0x74, 0x88, 0x82, 0x99, 0x6e, + 0xdb, 0xd0, 0xe2, 0x02, 0x3b, 0xee, 0x44, 0x23, 0x70, 0x14, + 0x1a, 0x37, 0x6b, 0x09, 0xb2, 0x0d, 0x79, 0x4b, 0xf6, 0xa3, + 0x63, 0x9c, 0xb3, 0x29, 0x29, 0xc6, 0xc9, 0x28, 0x0a, 0xce, + 0xf0, 0x96, 0xf9, 0xbb, 0x3b, 0x7a, 0xad, 0x7e, 0x12, 0x17, + 0xb6, 0xc6, 0x5f, 0x16, 0x23, 0xa7, 0xbf, 0xaa, 0x48, 0x60, + 0x60, 0x83, 0xdd, 0xf3, 0x2a, 0xdc, 0x92, 0x11, 0x62, 0x2c, + 0x8d, 0xd8, 0xb6, 0x77, 0x6e, 0x1a, 0x2a, 0xa5, 0x34, 0xda, + 0xad, 0x75, 0x64, 0x88, 0x73, 0x3f, 0x51, 0xfe, 0xca, 0x9f, + 0xe9, 0x65, 0x4a, 0x1e, 0xb6, 0x81, 0x05, 0xb8, 0x61, 0x01, + 0x1e, 0x36, 0x41, 0x6e, 0xd6, 0x70, 0xe3, 0xc9, 0xa4, 0xe7, + 0x1e, 0x6c, 0x55, 0xc6, 0xe8, 0xbf, 0xfe, 0xcf, 0x1e, 0x23, + 0x99, 0xca, 0xf0, 0xa7, 0x57, 0x3e, 0x8f, 0xf5, 0x6f, 0x0e, + 0xe9, 0xbc, 0xef, 0x28, 0x13, 0xe8, 0xf7, 0x2f, 0x5e, 0xc2, + 0xbe, 0xac, 0x79, 0x37, 0xad, 0xbb, 0xe2, 0x01, 0xf4, 0x63, + 0x7f, 0xef, 0x9a, 0xbc, 0x9b, 0xa7, 0x5a, 0x70, 0xfa, 0x02, + 0xc6, 0xa4, 0x4a, 0xbb, 0xab, 0xed, 0x41, 0x6c, 0x5a, 0x38, + 0xbf, 0xdb, 0xb1, 0x6d, 0xd7, 0xe2, 0x3a, 0xfe, 0x23, 0x3f, + 0x6c, 0xb0, 0x18, 0xce, 0xc8, 0xed, 0x82, 0x5d, 0x4d, 0x18, + 0x43, 0x89, 0x57, 0x06, 0xfd, 0xc2, 0x94, 0x0f, 0x95, 0xda, + 0x43, 0xa7, 0xbc, 0xc7, 0x61, 0x39, 0x71, 0x8f, 0x9d, 0x4c, + 0x1c, 0xe5, 0x80, 0x2d, 0x37, 0xd4, 0xd1, 0x4e, 0x9a, 0xa6, + 0x69, 0x44, 0xdf, 0xa3, 0x3d, 0x7b, 0x8c, 0x56, 0x71, 0x0d, + 0xf0, 0x7c, 0xbc, 0xcc, 0xde, 0x2c, 0x00, 0x3c, 0xe6, 0x44, + 0x75, 0xbe, 0x74, 0x23, 0x97, 0xdb, 0xde, 0x19, 0xb8, 0x43, + 0x3b, 0x20, 0x40, 0x3a, 0xa0, 0xde, 0x9c, 0x5e, 0x0d, 0x74, + 0x88, 0x9f, 0xd6, 0x37, 0x3d, 0x0f, 0xce, 0xac, 0xed, 0xb0, + 0x84, 0xf6, 0xe5, 0x23, 0x90, 0x04, 0x79, 0x27, 0x42, 0x01, + 0x9e, 0x40, 0x02, 0x72, 0xe1, 0xee, 0x80, 0x95, 0x69, 0x6f, + 0x44, 0x0c, 0x5c, 0x82, 0x23, 0x2b, 0x43, 0x7f, 0x55, 0xec, + 0xf8, 0xfb, 0x23, 0xed, 0x49, 0x92, 0x1d, 0xcf, 0xef, 0xcf, + 0x02, 0x04, 0xd8, 0x69, 0x7f, 0x3f, 0xf8, 0x8b, 0xee, 0x6f, + 0x30, 0x4d, 0x4b, 0x4e, 0xdb, 0x8a, 0xfc, 0xd3, 0xb5, 0x00, + 0x80, 0x52, 0x99, 0x9c, 0xab, 0x46, 0x94, 0x34, 0xca, 0x0e, + 0x30, 0xae, 0x0e, 0xfb, 0xfa, 0x18, 0x63, 0x8a, 0x26, 0x86, + 0xb9, 0x87, 0x05, 0xdc, 0x62, 0x64, 0x62, 0x3a, 0x53, 0x96, + 0x23, 0xc6, 0x6b, 0xf6, 0xb0, 0x7c, 0xca, 0x0c, 0x30, 0x90, + 0x85, 0x95, 0xb8, 0x7a, 0x1b, 0xa5, 0x3e, 0xb6, 0x08, 0x80, + 0xd5, 0x64, 0xb6, 0x65, 0x8d, 0x6a, 0xf3, 0x5a, 0x70, 0x4a, + 0x11, 0x79, 0x8a, 0xe0, 0xac, 0x29, 0x4e, 0x84, 0x1b, 0xdf, + 0xcb, 0xf8, 0xc4, 0x12, 0x3b, 0x6a, 0xad, 0x42, 0x07, 0x1a, + 0x76, 0x5e, 0xda, 0x5a, 0x23, 0x44, 0x1d, 0x2d, 0xc9, 0xec, + 0x07, 0x80, 0xdb, 0x6b, 0xf9, 0xb3, 0x7d, 0x60, 0xb5, 0xa9, + 0xe4, 0xac, 0x57, 0x1a, 0x62, 0x8b, 0x2e, 0x07, 0x40, 0x5d, + 0x72, 0xa6, 0x55, 0x43, 0xf9, 0xf4, 0xa5, 0xf7, 0xeb, 0x2d, + 0xf8, 0xf6, 0xe1, 0x97, 0x6d, 0x8c, 0x3c, 0xef, 0x9d, 0xf8, + 0xfc, 0xdf, 0xe2, 0xbd, 0xf4, 0x96, 0x69, 0x31, 0xbe, 0x69, + 0xe3, 0x73, 0x12, 0xab, 0x62, 0x8d, 0x8c, 0xbd, 0xf1, 0x25, + 0xd8, 0x9c, 0x13, 0x68, 0x9f, 0x31, 0xb5, 0x23, 0xb6, 0x16, + 0x1f, 0xea, 0x5d, 0x6e, 0x64, 0xdb, 0x02, 0xd4, 0x3f, 0x26, + 0x9a, 0x08, 0x44, 0xa8, 0x05, 0x39, 0x0c, 0xc2, 0x16, 0xe0, + 0x15, 0x71, 0xbf, 0xb4, 0x6e, 0x80, 0x61, 0xf2, 0x9b, 0x4f, + 0xb4, 0x12, 0x33, 0x4d, 0x6a, 0x86, 0xe2, 0xf0, 0xd9, 0x4d, + 0x45, 0x7a, 0x44, 0x4c, 0x3c, 0x6f, 0xa5, 0x31, 0x47, 0xcd, + 0x67, 0x0f, 0x43, 0xa4, 0x2e, 0xd6, 0xe6, 0xdd, 0xc2, 0xf0, + 0xa8, 0x7e, 0x0f, 0x1f, 0x59, 0x81, 0x3a, 0xde, 0xba, 0xa1, + 0x32, 0xe6, 0x98, 0x09, 0x9c, 0xc4, 0x2a, 0xc4, 0x1c, 0xf5, + 0x10, 0x31, 0xfb, 0xcf, 0x96, 0x55, 0x0f, 0xec, 0x5d, 0x63, + 0x4f, 0xcb, 0x74, 0x27, 0x35, 0xf3, 0x3a, 0x6b, 0x99, 0x30, + 0x47, 0xbb, 0xf9, 0x60, 0x08, 0x77, 0x7d, 0xef, 0xd2, 0x8c, + 0xbd, 0xa9, 0x39, 0xda, 0xc1, 0x6a, 0xdb, 0x0b, 0x71, 0x94, + 0xba, 0xe6, 0xc2, 0x09, 0xd6, 0x67, 0x00, 0xe2, 0x7c, 0x1b, + 0x8f, 0x89, 0x09, 0x91, 0xc1, 0xee, 0x51, 0xaf, 0x09, 0x8a, + 0x95, 0xed, 0xd9, 0x86, 0x2a, 0xf2, 0xc6, 0xfd, 0x72, 0x2f, + 0x3d, 0x18, 0x9f, 0x7f, 0x4a, 0x6a, 0x98, 0x9e, 0x6e, 0x48, + 0xe0, 0x69, 0x32, 0x97, 0x80, 0xe9, 0x7a, 0x2b, 0x5a, 0x5c, + 0xdb, 0xcb, 0x58, 0xf3, 0x8b, 0x7f, 0x2c, 0x05, 0x9f, 0xa6, + 0xbf, 0x98, 0xc8, 0x59, 0x20, 0x0e, 0x6c, 0xab, 0x99, 0x76, + 0xc8, 0xa2, 0xe9, 0x78, 0x31, 0xe8, 0xc1, 0x1f, 0xda, 0x9d, + 0x6a, 0xd3, 0x96, 0xbd, 0xbf, 0xda, 0x52, 0x2a, 0x3e, 0x48, + 0x2f, 0xd0, 0x1f, 0xac, 0x85, 0xf9, 0x9c, 0xa7, 0x17, 0xa9, + 0x25, 0x75, 0xb0, 0x54, 0x1c, 0x15, 0xc7, 0x17, 0x29, 0x79, + 0xb9, 0x90, 0x6a, 0x4d, 0x3a, 0x08, 0x26, 0x50, 0x30, 0xa7, + 0x41, 0x56, 0x9f, 0x0b, 0xb2, 0x6a, 0xfa, 0x1b, 0xa9, 0xfc, + 0xab, 0x50, 0x48, 0x85, 0x71, 0x0d, 0x68, 0x55, 0x91, 0x96, + 0xa2, 0x87, 0x51, 0xda, 0xf8, 0x4c, 0xfb, 0x63, 0x6a, 0x5f, + 0xa9, 0xcb, 0xf0, 0x12, 0xa5, 0x07, 0xb8, 0xe0, 0x95, 0xb7, + 0x81, 0x2a, 0x47, 0x1e, 0x34, 0x2c, 0xb5, 0xd0, 0x27, 0x84, + 0xee, 0x33, 0xd1, 0xc4, 0x73, 0xad, 0xc3, 0x05, 0xaa, 0x18, + 0x87, 0xb4, 0x96, 0x0c, 0x5f, 0xe2, 0xd0, 0x30, 0x00, 0xc7, + 0x22, 0x80, 0xe3, 0xd4, 0xf2, 0xd5, 0x55, 0x72, 0x59, 0x61, + 0x3f, 0xea, 0x4a, 0x5d, 0xf7, 0xb2, 0x44, 0x16, 0x96, 0x35, + 0x73, 0x33, 0x77, 0xee, 0xeb, 0x65, 0xef, 0x41, 0xc2, 0xe0, + 0xcc, 0x04, 0x09, 0x90, 0xa0, 0xe3, 0xec, 0x65, 0x8d, 0xe1, + 0xca, 0xa7, 0x75, 0xb4, 0x92, 0x98, 0xdd, 0x88, 0xcd, 0x48, + 0x0c, 0xf9, 0xb0, 0x45, 0xbd, 0x9f, 0x98, 0x8d, 0x5a, 0x50, + 0x47, 0x65, 0x68, 0x6f, 0xc3, 0x08, 0x78, 0x4c, 0xbc, 0xbc, + 0x8f, 0x16, 0x48, 0x69, 0x08, 0xf5, 0x03, 0xc5, 0x25, 0x9c, + 0x90, 0x3f, 0x54, 0x80, 0x7c, 0x9d, 0x90, 0x26, 0x77, 0x56, + 0xca, 0x1c, 0x89, 0xfe, 0xc8, 0xd1, 0x3c, 0x1b, 0xc4, 0x20, + 0x64, 0xc8, 0x61, 0x09, 0x1e, 0x36, 0x21, 0xcd, 0x94, 0xb3, + 0x03, 0xd9, 0xbc, 0x30, 0x37, 0xc6, 0xff, 0x98, 0xe2, 0x7b, + 0xc4, 0x02, 0xcf, 0x48, 0x78, 0x5c, 0xdd, 0xf8, 0x1b, 0x7b, + 0x1e, 0x21, 0xaf, 0x2a, 0x1b, 0xe3, 0x2c, 0x1a, 0x35, 0x33, + 0x8f, 0xff, 0xac, 0x41, 0x3d, 0x41, 0x81, 0x40, 0x13, 0x75, + 0xfc, 0x44, 0x9e, 0x57, 0xe5, 0x7d, 0x4c, 0x3a, 0x61, 0xa4, + 0x17, 0xcf, 0x28, 0x1a, 0x90, 0xc2, 0x56, 0xa9, 0x62, 0x03, + 0xd1, 0xbd, 0x77, 0x54, 0x70, 0x6a, 0x96, 0xce, 0xbd, 0xe0, + 0x86, 0x2f, 0x2c, 0x1b, 0x2c, 0x32, 0x94, 0xd3, 0x81, 0x9b, + 0x45, 0xb9, 0x90, 0x1f, 0xf7, 0xca, 0xb4, 0xe6, 0xcc, 0x29, + 0x12, 0x56, 0x8c, 0x2d, 0xd1, 0x68, 0x63, 0xf1, 0x6f, 0x39, + 0x16, 0xe9, 0x24, 0x55, 0xcd, 0x3d, 0xd7, 0x31, 0xa2, 0x46, + 0xee, 0x9c, 0xa2, 0x73, 0x8c, 0x75, 0x5c, 0xc5, 0x48, 0x12, + 0xd7, 0x18, 0x7e, 0xea, 0x05, 0x23, 0x11, 0x55, 0x1c, 0x07, + 0xdd, 0xa3, 0xca, 0x4b, 0x84, 0x4c, 0xf4, 0xf4, 0xc4, 0xd3, + 0xb1, 0xc2, 0x33, 0xb0, 0x9c, 0x85, 0x86, 0xb8, 0x80, 0xd6, + 0xe3, 0x76, 0xba, 0x38, 0x84, 0xca, 0xa8, 0x1e, 0x30, 0x48, + 0xf3, 0x8b, 0x37, 0xf1, 0x16, 0x78, 0x57, 0xcb, 0xf4, 0x4d, + 0x2c, 0x8f, 0xb8, 0x2c, 0xae, 0x4e, 0x31, 0x4d, 0xee, 0xb9, + 0x50, 0x31, 0xe4, 0x57, 0xd4, 0xaf, 0x3f, 0xf0, 0x8d, 0xdb, + 0xe6, 0x4b, 0x11, 0x68, 0x72, 0x81, 0xb8, 0xe3, 0x36, 0x19, + 0x96, 0xaf, 0x4f, 0x3d, 0x8a, 0x3d, 0x80, 0x5b, 0x68, 0xdb, + 0xb7, 0x3f, 0x97, 0x43, 0xca, 0x7d, 0x3c, 0x26, 0x81, 0x03, + 0xe1, 0xa9, 0x18, 0x79, 0x91, 0xbd, 0x0f, 0x01, 0x2a, 0x0e, + 0xbf, 0x92, 0x83, 0x51, 0x69, 0x08, 0x07, 0x82, 0x3b, 0x02, + 0x8c, 0x8c, 0x37, 0xd9, 0xfb, 0x43, 0xfe, 0x5c, 0x64, 0xe5, + 0x5a, 0x23, 0xe3, 0xec, 0x08, 0xa1, 0x82, 0x6a, 0x0b, 0x9f, + 0x7b, 0x78, 0x06, 0xad, 0xa0, 0xa7, 0x49, 0x6b, 0x67, 0x62, + 0x93, 0x0e, 0x17, 0xa3, 0x35, 0x22, 0x0f, 0x1b, 0x75, 0x30, + 0xff, 0xb1, 0xfc, 0xef, 0x2e, 0x05, 0x08, 0x28, 0x3d, 0x44, + 0x8a, 0x4a, 0x03, 0xe3, 0xde, 0xde, 0x87, 0x1a, 0xf9, 0xd9, + 0x14, 0x82, 0x4d, 0xe4, 0xe1, 0x96, 0xd2, 0x24, 0x22, 0x71, + 0xb3, 0x8d, 0x61, 0xae, 0xbc, 0x25, 0xc9, 0x56, 0xa7, 0xc7, + 0xaa, 0xa6, 0xdb, 0xfb, 0x53, 0x74, 0xa5, 0xe8, 0x24, 0x28, + 0xac, 0x5d, 0x50, 0x1d, 0x1d, 0x52, 0xa5, 0x25, 0xda, 0x6b, + 0xee, 0x4b, 0xba, 0x7d, 0x57, 0x62, 0xe0, 0xaa, 0xd1, 0xa6, + 0xe8, 0x93, 0x69, 0x9e, 0x69, 0x48, 0xa9, 0xf2, 0xc1, 0x43, + 0xfd, 0x80, 0x1a, 0x77, 0x5a, 0xca, 0x31, 0xb1, 0xc6, 0x8c, + 0x2f, 0xb0, 0xb7, 0x04, 0x38, 0x83, 0x76, 0xfc, 0x5f, 0x6b, + 0x62, 0xb2, 0xa4, 0x79, 0xc6, 0x61, 0x82, 0x48, 0xfc, 0x1b, + 0x54, 0x69, 0x13, 0xd5, 0x9b, 0x68, 0x12, 0x5b, 0x4a, 0xcf, + 0xa2, 0xb9, 0x50, 0x9f, 0xc0, 0xcc, 0xe6, 0x18, 0xc8, 0x04, + 0xaf, 0xc0, 0x5b, 0x1d, 0xa5, 0xa7, 0x4c, 0xbe, 0x79, 0xc7, + 0x51, 0xce, 0xbb, 0xfd, 0x2f, 0x65, 0x3e, 0x34, 0x69, 0xed, + 0x8d, 0xc6, 0x09, 0x62, 0x33, 0x12, 0x58, 0xf2, 0x4f, 0x96, + 0x8e, 0xb2, 0x93, 0xf8, 0xbf, 0xf8, 0xeb, 0x83, 0x4c, 0x3b, + 0xc1, 0x26, 0x4c, 0x45, 0x1f, 0x6e, 0x04, 0xe3, 0xcf, 0x13, + 0x3b, 0x34, 0xb4, 0x36, 0x48, 0x4a, 0xd2, 0x3d, 0x8a, 0xd6, + 0x57, 0x98, 0x74, 0x17, 0xba, 0x14, 0x3a, 0x58, 0x99, 0x68, + 0xad, 0x2b, 0x81, 0x6f, 0x57, 0x63, 0x52, 0x19, 0x36, 0xfe, + 0x1d, 0xcb, 0x73, 0xa1, 0x35, 0xb2, 0xf1, 0xf1, 0x26, 0x9e, + 0x2b, 0xf3, 0x1b, 0xcd, 0x10, 0xd7, 0x92, 0x29, 0x65, 0x1e, + 0x02, 0x8d, 0x08, 0x4e, 0x68, 0xd3, 0x70, 0xf7, 0x87, 0xe5, + 0x53, 0x04, 0xa4, 0x43, 0xc5, 0x30, 0x74, 0x4c, 0x5e, 0xe1, + 0xf6, 0xa8, 0xd8, 0x55, 0x95, 0xc8, 0x42, 0xd8, 0xfc, 0x3d, + 0x2e, 0x8d, 0x8f, 0x0a, 0x20, 0x76, 0xd3, 0x7a, 0xfb, 0x07, + 0xba, 0xd8, 0x07, 0xef, 0x29, 0x3a, 0x3d, 0x81, 0x90, 0xaf, + 0x0c, 0x13, 0x96, 0x72, 0xb6, 0x3c, 0x0e, 0x57, 0x96, 0x3e, + 0x51, 0x91, 0xdb, 0xb0, 0x2d, 0xdf, 0x31, 0x39, 0x21, 0x29, + 0xbb, 0x17, 0x0a, 0x23, 0x6b, 0xe4, 0xdc, 0x69, 0x27, 0xc4, + 0x20, 0x98, 0xfd, 0x34, 0xca, 0x7a, 0x66, 0x20, 0x58, 0xd2, + 0x36, 0x7f, 0x2b, 0xa7, 0xd1, 0xde, 0x6f, 0x36, 0xb4, 0xf2, + 0x3b, 0x20, 0x5d, 0x02 + }; + + if(target < CHIAKI_TARGET_PS4_10) + return CHIAKI_ERR_INVALID_DATA; + + const uint8_t *keys_a = chiaki_target_is_ps5(target) ? keys_a_ps5 : keys_a_ps4; + const uint8_t *keys_b = chiaki_target_is_ps5(target) ? keys_b_ps5 : keys_b_ps4; + + const uint8_t *key = &keys_a[(nonce[0] >> 3) * 0x70]; for(size_t i=0; i> 3) * 0x70]; - for(size_t i=0; i> 3) * 0x70]; + if(chiaki_target_is_ps5(target)) { - uint8_t v = (key[i] ^ morning[i]); - v += 0x21; - v += i; - v ^= nonce[i]; - bright[i] = v; + for(size_t i=0; ilog = log; session->quit_reason = CHIAKI_QUIT_REASON_NONE; - session->target = CHIAKI_TARGET_PS4_10; + session->target = connect_info->ps5 ? CHIAKI_TARGET_PS5_1 : CHIAKI_TARGET_PS4_10; ChiakiErrorCode err = chiaki_cond_init(&session->state_cond); if(err != CHIAKI_ERR_SUCCESS) @@ -366,9 +366,9 @@ static void *session_thread_func(void *arg) CHECK_STOP(quit); - CHIAKI_LOGI(session->log, "Starting session request"); + CHIAKI_LOGI(session->log, "Starting session request for %s", session->connect_info.ps5 ? "PS5" : "PS4"); - ChiakiTarget server_target = session->connect_info.ps5 ? CHIAKI_TARGET_PS5_UNKNOWN : CHIAKI_TARGET_PS4_UNKNOWN; + ChiakiTarget server_target = CHIAKI_TARGET_PS4_UNKNOWN; success = session_thread_request_session(session, &server_target) == CHIAKI_ERR_SUCCESS; if(!success && chiaki_target_is_unknown(server_target)) @@ -667,9 +667,13 @@ static ChiakiErrorCode session_thread_request_session(ChiakiSession *session, Ch "RP-Registkey: %s\r\n" "Rp-Version: %s\r\n" "\r\n"; - const char *path = (session->target == CHIAKI_TARGET_PS4_8 || session->target == CHIAKI_TARGET_PS4_9) - ? "/sce/rp/session" - : "/sie/ps4/rp/sess/init"; + const char *path; + if(session->target == CHIAKI_TARGET_PS4_8 || session->target == CHIAKI_TARGET_PS4_9) + path = "/sce/rp/session"; + else if(chiaki_target_is_ps5(session->target)) + path = "/sie/ps5/rp/sess/init"; + else + path = "/sie/ps4/rp/sess/init"; size_t regist_key_len = sizeof(session->connect_info.regist_key); for(size_t i=0; i Date: Thu, 17 Dec 2020 20:13:15 +0100 Subject: [PATCH 096/237] Update Protobuf --- lib/protobuf/takion.proto | 49 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 47 insertions(+), 2 deletions(-) diff --git a/lib/protobuf/takion.proto b/lib/protobuf/takion.proto index 9896e4f..748d70a 100644 --- a/lib/protobuf/takion.proto +++ b/lib/protobuf/takion.proto @@ -35,6 +35,9 @@ message TakionMessage { PERIODICTIMESTAMP = 27; SERVERSETTINGS = 28; DIRECTMESSAGE = 29; + MICCONNECTION = 30; + TAKIONPROTOCOLREQUEST = 31; + TAKIONPROTOCOLREQUESTACK = 32; } optional BigPayload big_payload = 2; @@ -64,6 +67,9 @@ message TakionMessage { optional PeriodicTimestampPayload periodic_timestamp_payload = 27; optional ServerSettingsPayload server_settings_payload = 28; optional DirectMessagePayload direct_message_payload = 29; + optional MicConnectionPayload mic_connection_payload = 30; + optional TakionProtocolRequestPayload takion_protocol_request = 31; + optional TakionProtocolRequestAckPayload takion_protocol_request_ack = 32; } message EventCode { @@ -249,6 +255,12 @@ message ResolutionPayload { required bytes video_header = 3; } +message AudioChannelPayload { + required uint32 audio_channel_type = 1; + required bytes audio_header = 2; + optional bool is_raw_pcm = 3; +} + message StreamInfoPayload { repeated ResolutionPayload resolution = 1; required bytes audio_header = 2; @@ -256,6 +268,7 @@ message StreamInfoPayload { optional uint32 afk_timeout = 4; optional uint32 afk_timeout_disconnect = 5; optional uint32 congestion_control_interval = 6; + repeated AudioChannelPayload audio_channel = 7; } message XmbCommandPayload { @@ -274,6 +287,8 @@ message ConnectionQualityPayload { optional uint32 upstream_bitrate = 2; optional float upstream_loss = 3; optional bool disable_upstream_audio = 4; + optional double rtt = 5; + optional uint64 loss = 6; } message PlayTimeLeftPayload { @@ -296,6 +311,8 @@ message ControllerConnectionPayload { DUALSHOCK4 = 2; VITA = 3; XINPUT = 4; + MOBILE = 5; + BOND = 6; } } @@ -338,6 +355,7 @@ message DeepLinkPayload { GAME_ALERT = 2; SYSTEM_SERVICE_STATUS = 3; DEBUG_VSH_MENU = 4; + RAW = 5; } optional uint32 request_id = 2; @@ -345,15 +363,30 @@ message DeepLinkPayload { optional string invitation_id = 4; optional string session_id = 5; optional string item_id = 6; - optional string is_system_ui_qverlaid = 7; + optional string is_system_ui_overlaid = 7; optional uint32 result = 8; optional bool should_show = 9; + optional string raw_data = 10; +} + +message MicInfoPayload { + required MicInfoType mic_info_type = 1; + enum MicInfoType { + MIC_CONNECT = 0; + MIC_MUTE = 1; + } + + optional uint32 controller_id = 2; + optional bool connected = 3; + optional bool muted = 4; + optional bool result = 5; } message DirectMessagePayload { required DirectMessageType direct_message_type = 1; enum DirectMessageType { DEEPLINK = 0; + MICINFO = 1; } required Destination destination = 2; @@ -365,7 +398,19 @@ message DirectMessagePayload { optional bytes data = 3; } +message MicConnectionPayload { + required int32 controller_id = 1; + required bool connected = 2; + optional bool result = 3; +} +message TakionProtocolRequestPayload { + repeated uint32 supported_takion_versions = 1; +} + +message TakionProtocolRequestAckPayload { + optional uint32 takion_protocol_version = 1; +} message SenkushaPayload { required Command command = 1; @@ -412,4 +457,4 @@ message SenkushaClientMtuCommand { required uint32 mtu_req = 2; required bool state = 3; optional uint32 mtu_down = 4; -} \ No newline at end of file +} From a8d2c6c29fd0065156fe8c1bdd3e81102a96a837 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Sun, 20 Dec 2020 17:03:05 +0100 Subject: [PATCH 097/237] Begin Stream v12 --- lib/include/chiaki/feedback.h | 14 +++++++++++--- lib/include/chiaki/launchspec.h | 2 ++ lib/include/chiaki/takion.h | 1 + lib/src/feedback.c | 10 +++++++++- lib/src/launchspec.c | 18 +++++++++++++++--- lib/src/session.c | 5 +++-- lib/src/streamconnection.c | 7 +++++-- lib/src/takion.c | 23 ++++++++++++++++++----- 8 files changed, 64 insertions(+), 16 deletions(-) diff --git a/lib/include/chiaki/feedback.h b/lib/include/chiaki/feedback.h index 6573a83..264af5c 100644 --- a/lib/include/chiaki/feedback.h +++ b/lib/include/chiaki/feedback.h @@ -21,13 +21,21 @@ typedef struct chiaki_feedback_state_t int16_t right_y; } ChiakiFeedbackState; -#define CHIAKI_FEEDBACK_STATE_BUF_SIZE 0x19 +#define CHIAKI_FEEDBACK_STATE_BUF_SIZE_MAX 0x1c + +#define CHIAKI_FEEDBACK_STATE_BUF_SIZE_V9 0x19 /** - * @param buf buffer of at least CHIAKI_FEEDBACK_STATE_BUF_SIZE + * @param buf buffer of at least CHIAKI_FEEDBACK_STATE_BUF_SIZE_V9 */ -CHIAKI_EXPORT void chiaki_feedback_state_format(uint8_t *buf, ChiakiFeedbackState *state); +CHIAKI_EXPORT void chiaki_feedback_state_format_v9(uint8_t *buf, ChiakiFeedbackState *state); +#define CHIAKI_FEEDBACK_STATE_BUF_SIZE_V12 0x1c + +/** + * @param buf buffer of at least CHIAKI_FEEDBACK_STATE_BUF_SIZE_V12 + */ +CHIAKI_EXPORT void chiaki_feedback_state_format_v12(uint8_t *buf, ChiakiFeedbackState *state); #define CHIAKI_HISTORY_EVENT_SIZE_MAX 0x5 diff --git a/lib/include/chiaki/launchspec.h b/lib/include/chiaki/launchspec.h index 0606280..770eb2a 100644 --- a/lib/include/chiaki/launchspec.h +++ b/lib/include/chiaki/launchspec.h @@ -14,12 +14,14 @@ extern "C" { typedef struct chiaki_launch_spec_t { + ChiakiTarget target; unsigned int mtu; unsigned int rtt; uint8_t *handshake_key; unsigned int width; unsigned int height; unsigned int max_fps; + ChiakiCodec codec; unsigned int bw_kbps_sent; } ChiakiLaunchSpec; diff --git a/lib/include/chiaki/takion.h b/lib/include/chiaki/takion.h index 01b5521..dba5872 100644 --- a/lib/include/chiaki/takion.h +++ b/lib/include/chiaki/takion.h @@ -110,6 +110,7 @@ typedef struct chiaki_takion_connect_info_t typedef struct chiaki_takion_t { ChiakiLog *log; + uint8_t version; /** * Whether encryption should be used. diff --git a/lib/src/feedback.c b/lib/src/feedback.c index 05c29b9..d5a46bc 100644 --- a/lib/src/feedback.c +++ b/lib/src/feedback.c @@ -10,7 +10,7 @@ #endif #include -CHIAKI_EXPORT void chiaki_feedback_state_format(uint8_t *buf, ChiakiFeedbackState *state) +CHIAKI_EXPORT void chiaki_feedback_state_format_v9(uint8_t *buf, ChiakiFeedbackState *state) { buf[0x0] = 0xa0; // TODO buf[0x1] = 0xff; // TODO @@ -35,6 +35,14 @@ CHIAKI_EXPORT void chiaki_feedback_state_format(uint8_t *buf, ChiakiFeedbackStat *((chiaki_unaligned_uint16_t *)(buf + 0x17)) = htons((uint16_t)state->right_y); } +CHIAKI_EXPORT void chiaki_feedback_state_format_v12(uint8_t *buf, ChiakiFeedbackState *state) +{ + chiaki_feedback_state_format_v9(buf, state); + buf[0x10] = 0x0; + buf[0x1a] = 0x0; + buf[0x1b] = 0x1; // 1 for Shock, 0 for Sense +} + CHIAKI_EXPORT ChiakiErrorCode chiaki_feedback_history_event_set_button(ChiakiFeedbackHistoryEvent *event, uint64_t button, uint8_t state) { // some buttons use a third byte for the state, some don't diff --git a/lib/src/launchspec.c b/lib/src/launchspec.c index d3b71b0..9da96ba 100644 --- a/lib/src/launchspec.c +++ b/lib/src/launchspec.c @@ -58,7 +58,9 @@ static const char launchspec_fmt[] = "\"region\":\"US\"," "\"languagesUsed\":[\"en\",\"jp\"]" "}," - "\"handshakeKey\":\"%s\"" // 6 + "%s" // 6 + "%s" // 7 + "\"handshakeKey\":\"%s\"" // 8 "}"; CHIAKI_EXPORT int chiaki_launchspec_format(char *buf, size_t buf_size, ChiakiLaunchSpec *launch_spec) @@ -68,10 +70,20 @@ CHIAKI_EXPORT int chiaki_launchspec_format(char *buf, size_t buf_size, ChiakiLau if(err != CHIAKI_ERR_SUCCESS) return -1; + char *extras[2]; + if(chiaki_target_is_ps5(launch_spec->target)) // TODO: probably also for ps4, but only 12 + { + extras[0] = "\"videoCodec\":\"avc\","; // TODO: hevc too + extras[1] = "\"dynamicRange\":\"SDR\","; // TODO: HDR too + } + else + extras[0] = extras[1] = ""; + int written = snprintf(buf, buf_size, launchspec_fmt, launch_spec->width, launch_spec->height, launch_spec->max_fps, - launch_spec->bw_kbps_sent, launch_spec->mtu, launch_spec->rtt, handshake_key_b64); + launch_spec->bw_kbps_sent, launch_spec->mtu, launch_spec->rtt, + extras[0], extras[1], handshake_key_b64); if(written < 0 || written >= buf_size) return -1; return written; -} \ No newline at end of file +} diff --git a/lib/src/session.c b/lib/src/session.c index 067344a..8a0d06e 100644 --- a/lib/src/session.c +++ b/lib/src/session.c @@ -370,6 +370,7 @@ static void *session_thread_func(void *arg) ChiakiTarget server_target = CHIAKI_TARGET_PS4_UNKNOWN; success = session_thread_request_session(session, &server_target) == CHIAKI_ERR_SUCCESS; + // TODO: Check Network error return if(!success && chiaki_target_is_unknown(server_target)) { @@ -653,7 +654,7 @@ static ChiakiErrorCode session_thread_request_session(ChiakiSession *session, Ch CHIAKI_LOGE(session->log, "Session request connect failed eventually."); if(session->quit_reason == CHIAKI_QUIT_REASON_NONE) session->quit_reason = CHIAKI_QUIT_REASON_SESSION_REQUEST_UNKNOWN; - return false; + return CHIAKI_ERR_NETWORK; } CHIAKI_LOGI(session->log, "Connected to %s:%d", session->connect_info.hostname, SESSION_PORT); @@ -690,7 +691,7 @@ static ChiakiErrorCode session_thread_request_session(ChiakiSession *session, Ch { CHIAKI_SOCKET_CLOSE(session_sock); session->quit_reason = CHIAKI_QUIT_REASON_SESSION_REQUEST_UNKNOWN; - return false; + return CHIAKI_ERR_UNKNOWN; } const char *rp_version_str = chiaki_rp_version_string(session->target); diff --git a/lib/src/streamconnection.c b/lib/src/streamconnection.c index 25caeb6..c8ed43c 100644 --- a/lib/src/streamconnection.c +++ b/lib/src/streamconnection.c @@ -1,6 +1,7 @@ // SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL +#include "chiaki/common.h" #include #include #include @@ -142,7 +143,7 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_stream_connection_run(ChiakiStreamConnectio takion_info.ip_dontfrag = false; takion_info.enable_crypt = true; - takion_info.protocol_version = 9; + takion_info.protocol_version = chiaki_target_is_ps5(session->target) ? 12 : 9; takion_info.cb = stream_connection_takion_cb; takion_info.cb_user = stream_connection; @@ -694,6 +695,7 @@ static ChiakiErrorCode stream_connection_send_big(ChiakiStreamConnection *stream ChiakiSession *session = stream_connection->session; ChiakiLaunchSpec launch_spec; + launch_spec.target = session->target; launch_spec.mtu = session->mtu_in; launch_spec.rtt = session->rtt_us / 1000; launch_spec.handshake_key = session->handshake_key; @@ -701,6 +703,7 @@ static ChiakiErrorCode stream_connection_send_big(ChiakiStreamConnection *stream launch_spec.width = session->connect_info.video_profile.width; launch_spec.height = session->connect_info.video_profile.height; launch_spec.max_fps = session->connect_info.video_profile.max_fps; + launch_spec.codec = session->connect_info.video_profile.codec; launch_spec.bw_kbps_sent = session->connect_info.video_profile.bitrate; union @@ -755,7 +758,7 @@ static ChiakiErrorCode stream_connection_send_big(ChiakiStreamConnection *stream msg.type = tkproto_TakionMessage_PayloadType_BIG; msg.has_big_payload = true; - msg.big_payload.client_version = 9; + msg.big_payload.client_version = stream_connection->takion.version; msg.big_payload.session_key.arg = session->session_id; msg.big_payload.session_key.funcs.encode = chiaki_pb_encode_string; msg.big_payload.launch_spec.arg = launch_spec_buf.b64; diff --git a/lib/src/takion.c b/lib/src/takion.c index 3e4a463..3439410 100644 --- a/lib/src/takion.c +++ b/lib/src/takion.c @@ -1,5 +1,6 @@ // SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL +#include "chiaki/feedback.h" #include #include #include @@ -180,17 +181,19 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_takion_connect(ChiakiTakion *takion, Chiaki ChiakiErrorCode ret = CHIAKI_ERR_SUCCESS; takion->log = info->log; + takion->version = info->protocol_version; - switch(info->protocol_version) + switch(takion->version) { case 7: takion->av_packet_parse = chiaki_takion_v7_av_packet_parse; break; case 9: + case 12: takion->av_packet_parse = chiaki_takion_v9_av_packet_parse; break; default: - CHIAKI_LOGE(takion->log, "Unknown Takion Protocol Version %u", (unsigned int)info->protocol_version); + CHIAKI_LOGE(takion->log, "Unknown Takion Protocol Version %u", (unsigned int)takion->version); return CHIAKI_ERR_INVALID_DATA; } @@ -541,14 +544,24 @@ beach: CHIAKI_EXPORT ChiakiErrorCode chiaki_takion_send_feedback_state(ChiakiTakion *takion, ChiakiSeqNum16 seq_num, ChiakiFeedbackState *feedback_state) { - uint8_t buf[0xc + CHIAKI_FEEDBACK_STATE_BUF_SIZE]; + uint8_t buf[0xc + CHIAKI_FEEDBACK_STATE_BUF_SIZE_MAX]; buf[0] = TAKION_PACKET_TYPE_FEEDBACK_STATE; *((chiaki_unaligned_uint16_t *)(buf + 1)) = htons(seq_num); buf[3] = 0; // TODO *((chiaki_unaligned_uint32_t *)(buf + 4)) = 0; // key pos *((chiaki_unaligned_uint32_t *)(buf + 8)) = 0; // gmac - chiaki_feedback_state_format(buf + 0xc, feedback_state); - return takion_send_feedback_packet(takion, buf, sizeof(buf)); + size_t buf_sz; + if(takion->version <= 9) + { + buf_sz = CHIAKI_FEEDBACK_STATE_BUF_SIZE_V9; + chiaki_feedback_state_format_v9(buf + 0xc, feedback_state); + } + else + { + buf_sz = CHIAKI_FEEDBACK_STATE_BUF_SIZE_V12; + chiaki_feedback_state_format_v9(buf + 0xc, feedback_state); + } + return takion_send_feedback_packet(takion, buf, buf_sz); } CHIAKI_EXPORT ChiakiErrorCode chiaki_takion_send_feedback_history(ChiakiTakion *takion, ChiakiSeqNum16 seq_num, uint8_t *payload, size_t payload_size) From d4e1aa3b609fbc78c8fb3eea5371d2e7c40c11ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Mon, 21 Dec 2020 17:24:37 +0100 Subject: [PATCH 098/237] Fix Audio Parsing for PS5 --- lib/include/chiaki/takion.h | 7 +++++++ lib/src/audioreceiver.c | 4 +++- lib/src/takion.c | 27 ++++++++++++++++++++++++--- 3 files changed, 34 insertions(+), 4 deletions(-) diff --git a/lib/include/chiaki/takion.h b/lib/include/chiaki/takion.h index dba5872..3f7e96f 100644 --- a/lib/include/chiaki/takion.h +++ b/lib/include/chiaki/takion.h @@ -45,6 +45,8 @@ typedef struct chiaki_takion_av_packet_t uint64_t key_pos; + uint8_t byte_before_audio_data; + uint8_t *data; // not owned size_t data_size; } ChiakiTakionAVPacket; @@ -226,6 +228,11 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_takion_send_feedback_history(ChiakiTakion * CHIAKI_EXPORT ChiakiErrorCode chiaki_takion_v9_av_packet_parse(ChiakiTakionAVPacket *packet, ChiakiKeyState *key_state, uint8_t *buf, size_t buf_size); +#define CHIAKI_TAKION_V12_AV_HEADER_SIZE_VIDEO 0x17 +#define CHIAKI_TAKION_V12_AV_HEADER_SIZE_AUDIO 0x13 + +CHIAKI_EXPORT ChiakiErrorCode chiaki_takion_v12_av_packet_parse(ChiakiTakionAVPacket *packet, ChiakiKeyState *key_state, uint8_t *buf, size_t buf_size); + #define CHIAKI_TAKION_V7_AV_HEADER_SIZE_BASE 0x12 #define CHIAKI_TAKION_V7_AV_HEADER_SIZE_VIDEO_ADD 0x3 #define CHIAKI_TAKION_V7_AV_HEADER_SIZE_NALU_INFO_STRUCTS_ADD 0x3 diff --git a/lib/src/audioreceiver.c b/lib/src/audioreceiver.c index 9aeaa61..5808e4c 100644 --- a/lib/src/audioreceiver.c +++ b/lib/src/audioreceiver.c @@ -78,7 +78,9 @@ CHIAKI_EXPORT void chiaki_audio_receiver_av_packet(ChiakiAudioReceiver *audio_re if(packet->data_size != (size_t)unit_size * (size_t)packet->units_in_frame_total) { - CHIAKI_LOGE(audio_receiver->log, "Audio AV Packet size mismatch"); + CHIAKI_LOGE(audio_receiver->log, "Audio AV Packet size mismatch %#llx vs %#llx", + (unsigned long long)packet->data_size, + (unsigned long long)(unit_size * packet->units_in_frame_total)); return; } diff --git a/lib/src/takion.c b/lib/src/takion.c index 3439410..934f151 100644 --- a/lib/src/takion.c +++ b/lib/src/takion.c @@ -189,9 +189,11 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_takion_connect(ChiakiTakion *takion, Chiaki takion->av_packet_parse = chiaki_takion_v7_av_packet_parse; break; case 9: - case 12: takion->av_packet_parse = chiaki_takion_v9_av_packet_parse; break; + case 12: + takion->av_packet_parse = chiaki_takion_v12_av_packet_parse; + break; default: CHIAKI_LOGE(takion->log, "Unknown Takion Protocol Version %u", (unsigned int)takion->version); return CHIAKI_ERR_INVALID_DATA; @@ -1243,7 +1245,7 @@ static void takion_handle_packet_av(ChiakiTakion *takion, uint8_t base_type, uin } } -CHIAKI_EXPORT ChiakiErrorCode chiaki_takion_v9_av_packet_parse(ChiakiTakionAVPacket *packet, ChiakiKeyState *key_state, uint8_t *buf, size_t buf_size) +static ChiakiErrorCode av_packet_parse(bool v12, ChiakiTakionAVPacket *packet, ChiakiKeyState *key_state, uint8_t *buf, size_t buf_size) { memset(packet, 0, sizeof(ChiakiTakionAVPacket)); @@ -1261,7 +1263,9 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_takion_v9_av_packet_parse(ChiakiTakionAVPac uint8_t *av = buf+1; size_t av_size = buf_size-1; - size_t av_header_size = packet->is_video ? CHIAKI_TAKION_V9_AV_HEADER_SIZE_VIDEO : CHIAKI_TAKION_V9_AV_HEADER_SIZE_AUDIO; + size_t av_header_size = v12 + ? (packet->is_video ? CHIAKI_TAKION_V12_AV_HEADER_SIZE_VIDEO : CHIAKI_TAKION_V12_AV_HEADER_SIZE_AUDIO) + : (packet->is_video ? CHIAKI_TAKION_V9_AV_HEADER_SIZE_VIDEO : CHIAKI_TAKION_V9_AV_HEADER_SIZE_AUDIO); if(av_size < av_header_size + 1) return CHIAKI_ERR_BUF_TOO_SMALL; @@ -1320,12 +1324,29 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_takion_v9_av_packet_parse(ChiakiTakionAVPac av_size -= 3; } + if(v12 && !packet->is_video) + { + packet->byte_before_audio_data = *av; + av += 1; + av_size -= 1; + } + packet->data = av; packet->data_size = av_size; return CHIAKI_ERR_SUCCESS; } +CHIAKI_EXPORT ChiakiErrorCode chiaki_takion_v9_av_packet_parse(ChiakiTakionAVPacket *packet, ChiakiKeyState *key_state, uint8_t *buf, size_t buf_size) +{ + return av_packet_parse(false, packet, key_state, buf, buf_size); +} + +CHIAKI_EXPORT ChiakiErrorCode chiaki_takion_v12_av_packet_parse(ChiakiTakionAVPacket *packet, ChiakiKeyState *key_state, uint8_t *buf, size_t buf_size) +{ + return av_packet_parse(true, packet, key_state, buf, buf_size); +} + CHIAKI_EXPORT ChiakiErrorCode chiaki_takion_v7_av_packet_format_header(uint8_t *buf, size_t buf_size, size_t *header_size_out, ChiakiTakionAVPacket *packet) { size_t header_size = CHIAKI_TAKION_V7_AV_HEADER_SIZE_BASE; From e2d1c110640f37492121bc4108a76b91f7f90370 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Tue, 22 Dec 2020 14:28:36 +0100 Subject: [PATCH 099/237] Finish new LaunchSpec --- gui/src/settings.cpp | 3 ++- lib/include/chiaki/common.h | 10 ++++++++++ lib/src/launchspec.c | 22 ++++++++++++++-------- 3 files changed, 26 insertions(+), 9 deletions(-) diff --git a/gui/src/settings.cpp b/gui/src/settings.cpp index e08ec57..3f637bb 100644 --- a/gui/src/settings.cpp +++ b/gui/src/settings.cpp @@ -204,11 +204,12 @@ void Settings::SetAudioBufferSize(unsigned int size) ChiakiConnectVideoProfile Settings::GetVideoProfile() { - ChiakiConnectVideoProfile profile; + ChiakiConnectVideoProfile profile = {}; chiaki_connect_video_profile_preset(&profile, GetResolution(), GetFPS()); unsigned int bitrate = GetBitrate(); if(bitrate) profile.bitrate = bitrate; + profile.codec = CHIAKI_CODEC_H264; // TODO: add a setting return profile; } diff --git a/lib/include/chiaki/common.h b/lib/include/chiaki/common.h index 8ad249b..b28f696 100644 --- a/lib/include/chiaki/common.h +++ b/lib/include/chiaki/common.h @@ -89,6 +89,16 @@ typedef enum CHIAKI_CODEC_H265_HDR } ChiakiCodec; +static inline bool chiaki_codec_is_h265(ChiakiCodec codec) +{ + return codec == CHIAKI_CODEC_H265 || codec == CHIAKI_CODEC_H265_HDR; +} + +static inline bool chiaki_codec_is_hdr(ChiakiCodec codec) +{ + return codec == CHIAKI_CODEC_H265_HDR; +} + #ifdef __cplusplus } #endif diff --git a/lib/src/launchspec.c b/lib/src/launchspec.c index 9da96ba..abed382 100644 --- a/lib/src/launchspec.c +++ b/lib/src/launchspec.c @@ -50,7 +50,8 @@ static const char launchspec_fmt[] = "\"connectedControllers\":[\"xinput\",\"ds3\",\"ds4\"]," "\"yuvCoefficient\":\"bt601\"," "\"videoEncoderProfile\":\"hw4.1\"," - "\"audioEncoderProfile\":\"audio1\"" + "\"audioEncoderProfile\":\"audio1\"" // 6 + "%s" "}," "\"userProfile\":{" "\"onlineId\":\"psnId\"," @@ -58,9 +59,9 @@ static const char launchspec_fmt[] = "\"region\":\"US\"," "\"languagesUsed\":[\"en\",\"jp\"]" "}," - "%s" // 6 "%s" // 7 - "\"handshakeKey\":\"%s\"" // 8 + "%s" // 8 + "\"handshakeKey\":\"%s\"" // 9 "}"; CHIAKI_EXPORT int chiaki_launchspec_format(char *buf, size_t buf_size, ChiakiLaunchSpec *launch_spec) @@ -70,19 +71,24 @@ CHIAKI_EXPORT int chiaki_launchspec_format(char *buf, size_t buf_size, ChiakiLau if(err != CHIAKI_ERR_SUCCESS) return -1; - char *extras[2]; + char *extras[3]; if(chiaki_target_is_ps5(launch_spec->target)) // TODO: probably also for ps4, but only 12 { - extras[0] = "\"videoCodec\":\"avc\","; // TODO: hevc too - extras[1] = "\"dynamicRange\":\"SDR\","; // TODO: HDR too + extras[0] = ",\"adaptiveStreamMode\": \"resize\""; + extras[1] = chiaki_codec_is_h265(launch_spec->codec) + ? "\"videoCodec\":\"hevc\"," + : "\"videoCodec\":\"avc\","; + extras[2] = chiaki_codec_is_hdr(launch_spec->codec) + ? "\"dynamicRange\":\"HDR\"," + : "\"dynamicRange\":\"SDR\","; } else - extras[0] = extras[1] = ""; + extras[0] = extras[1] = extras[2] = ""; int written = snprintf(buf, buf_size, launchspec_fmt, launch_spec->width, launch_spec->height, launch_spec->max_fps, launch_spec->bw_kbps_sent, launch_spec->mtu, launch_spec->rtt, - extras[0], extras[1], handshake_key_b64); + extras[0], extras[1], extras[2], handshake_key_b64); if(written < 0 || written >= buf_size) return -1; return written; From 65add80ec63087fed119f94080e35f8b09f36f67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Fri, 25 Dec 2020 14:44:51 +0100 Subject: [PATCH 100/237] Add new Console SVG --- assets/console2.svg | 81 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 assets/console2.svg diff --git a/assets/console2.svg b/assets/console2.svg new file mode 100644 index 0000000..39fb096 --- /dev/null +++ b/assets/console2.svg @@ -0,0 +1,81 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + + + From 8904c86a6da646307356f67d23911c8a01664c34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Fri, 25 Dec 2020 14:48:09 +0100 Subject: [PATCH 101/237] Add Discovery and Wakeup for PS5 --- cli/src/discover.c | 4 ++-- cli/src/wakeup.c | 2 +- gui/include/discoverymanager.h | 2 +- gui/src/discoverymanager.cpp | 5 ++--- gui/src/mainwindow.cpp | 3 ++- lib/include/chiaki/discovery.h | 8 +++++--- lib/src/discovery.c | 14 +++++++++----- lib/src/discoveryservice.c | 22 ++++++++++++++++++++-- switch/src/discoverymanager.cpp | 1 - 9 files changed, 42 insertions(+), 19 deletions(-) diff --git a/cli/src/discover.c b/cli/src/discover.c index 3ebb9d1..503b53a 100644 --- a/cli/src/discover.c +++ b/cli/src/discover.c @@ -142,7 +142,7 @@ CHIAKI_EXPORT int chiaki_cli_cmd_discover(ChiakiLog *log, int argc, char *argv[] return 1; } - ((struct sockaddr_in *)host_addr)->sin_port = htons(CHIAKI_DISCOVERY_PORT); // TODO: IPv6 + ((struct sockaddr_in *)host_addr)->sin_port = htons(CHIAKI_DISCOVERY_PORT_PS4); // TODO: IPv6, PS5, should probably use the service ChiakiDiscoveryPacket packet; memset(&packet, 0, sizeof(packet)); @@ -151,7 +151,7 @@ CHIAKI_EXPORT int chiaki_cli_cmd_discover(ChiakiLog *log, int argc, char *argv[] chiaki_discovery_send(&discovery, &packet, host_addr, host_addr_len); while(1) - sleep(1); + sleep(1); // TODO: wtf return 0; } diff --git a/cli/src/wakeup.c b/cli/src/wakeup.c index cc7dcde..cf5ab45 100644 --- a/cli/src/wakeup.c +++ b/cli/src/wakeup.c @@ -73,5 +73,5 @@ CHIAKI_EXPORT int chiaki_cli_cmd_wakeup(ChiakiLog *log, int argc, char *argv[]) uint64_t credential = (uint64_t)strtoull(arguments.registkey, NULL, 16); - return chiaki_discovery_wakeup(log, NULL, arguments.host, credential); + return chiaki_discovery_wakeup(log, NULL, arguments.host, credential, false); } diff --git a/gui/include/discoverymanager.h b/gui/include/discoverymanager.h index 67093de..f03a5e6 100644 --- a/gui/include/discoverymanager.h +++ b/gui/include/discoverymanager.h @@ -44,7 +44,7 @@ class DiscoveryManager : public QObject void SetActive(bool active); - void SendWakeup(const QString &host, const QByteArray ®ist_key); + void SendWakeup(const QString &host, const QByteArray ®ist_key, bool ps5); const QList GetHosts() const { return hosts; } diff --git a/gui/src/discoverymanager.cpp b/gui/src/discoverymanager.cpp index f85dc88..042e68b 100644 --- a/gui/src/discoverymanager.cpp +++ b/gui/src/discoverymanager.cpp @@ -58,7 +58,6 @@ void DiscoveryManager::SetActive(bool active) sockaddr_in addr = {}; addr.sin_family = AF_INET; - addr.sin_port = htons(CHIAKI_DISCOVERY_PORT); addr.sin_addr.s_addr = 0xffffffff; // 255.255.255.255 options.send_addr = reinterpret_cast(&addr); options.send_addr_size = sizeof(addr); @@ -80,7 +79,7 @@ void DiscoveryManager::SetActive(bool active) } -void DiscoveryManager::SendWakeup(const QString &host, const QByteArray ®ist_key) +void DiscoveryManager::SendWakeup(const QString &host, const QByteArray ®ist_key, bool ps5) { QByteArray key = regist_key; for(size_t i=0; iGetHostAddr(), server->registered_host.GetRPRegistKey()); + discovery_manager.SendWakeup(server->GetHostAddr(), server->registered_host.GetRPRegistKey(), + chiaki_target_is_ps5(server->registered_host.GetTarget())); } catch(const Exception &e) { diff --git a/lib/include/chiaki/discovery.h b/lib/include/chiaki/discovery.h index b9fa2ec..50364ac 100644 --- a/lib/include/chiaki/discovery.h +++ b/lib/include/chiaki/discovery.h @@ -21,8 +21,10 @@ typedef unsigned short sa_family_t; extern "C" { #endif -#define CHIAKI_DISCOVERY_PORT 987 -#define CHIAKI_DISCOVERY_PROTOCOL_VERSION "00020020" +#define CHIAKI_DISCOVERY_PORT_PS4 987 +#define CHIAKI_DISCOVERY_PROTOCOL_VERSION_PS4 "00020020" +#define CHIAKI_DISCOVERY_PORT_PS5 9302 +#define CHIAKI_DISCOVERY_PROTOCOL_VERSION_PS5 "00030010" typedef enum chiaki_discovery_cmd_t { @@ -101,7 +103,7 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_discovery_thread_stop(ChiakiDiscoveryThread * Convenience function to send a wakeup packet * @param discovery Discovery to send the packet on. May be NULL, in which case a new temporary Discovery will be created */ -CHIAKI_EXPORT ChiakiErrorCode chiaki_discovery_wakeup(ChiakiLog *log, ChiakiDiscovery *discovery, const char *host, uint64_t user_credential); +CHIAKI_EXPORT ChiakiErrorCode chiaki_discovery_wakeup(ChiakiLog *log, ChiakiDiscovery *discovery, const char *host, uint64_t user_credential, bool ps5); #ifdef __cplusplus } diff --git a/lib/src/discovery.c b/lib/src/discovery.c index fb910a4..4771456 100644 --- a/lib/src/discovery.c +++ b/lib/src/discovery.c @@ -34,12 +34,13 @@ const char *chiaki_discovery_host_state_string(ChiakiDiscoveryHostState state) CHIAKI_EXPORT int chiaki_discovery_packet_fmt(char *buf, size_t buf_size, ChiakiDiscoveryPacket *packet) { - const char *version_str = packet->protocol_version ? packet->protocol_version : CHIAKI_DISCOVERY_PROTOCOL_VERSION; + if(!packet->protocol_version) + return -1; switch(packet->cmd) { case CHIAKI_DISCOVERY_CMD_SRCH: return snprintf(buf, buf_size, "SRCH * HTTP/1.1\ndevice-discovery-protocol-version:%s\n", - version_str); + packet->protocol_version); case CHIAKI_DISCOVERY_CMD_WAKEUP: return snprintf(buf, buf_size, "WAKEUP * HTTP/1.1\n" @@ -49,7 +50,7 @@ CHIAKI_EXPORT int chiaki_discovery_packet_fmt(char *buf, size_t buf_size, Chiaki "app-type:r\n" "user-credential:%llu\n" "device-discovery-protocol-version:%s\n", - (unsigned long long)packet->user_credential, version_str); + (unsigned long long)packet->user_credential, packet->protocol_version); default: return -1; } @@ -179,6 +180,8 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_discovery_send(ChiakiDiscovery *discovery, if((size_t)len >= sizeof(buf)) return CHIAKI_ERR_BUF_TOO_SMALL; + CHIAKI_LOGV(discovery->log, "Discovery sending:"); + chiaki_log_hexdump(discovery->log, CHIAKI_LOG_VERBOSE, (const uint8_t *)buf, (size_t)len + 1); int rc = sendto_broadcast(discovery->log, discovery->socket, buf, (size_t)len + 1, 0, addr, addr_size); if(rc < 0) { @@ -280,7 +283,7 @@ static void *discovery_thread_func(void *user) return NULL; } -CHIAKI_EXPORT ChiakiErrorCode chiaki_discovery_wakeup(ChiakiLog *log, ChiakiDiscovery *discovery, const char *host, uint64_t user_credential) +CHIAKI_EXPORT ChiakiErrorCode chiaki_discovery_wakeup(ChiakiLog *log, ChiakiDiscovery *discovery, const char *host, uint64_t user_credential, bool ps5) { struct addrinfo *addrinfos; int r = getaddrinfo(host, NULL, NULL, &addrinfos); // TODO: this blocks, use something else @@ -311,10 +314,11 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_discovery_wakeup(ChiakiLog *log, ChiakiDisc return CHIAKI_ERR_UNKNOWN; } - ((struct sockaddr_in *)&addr)->sin_port = htons(CHIAKI_DISCOVERY_PORT); + ((struct sockaddr_in *)&addr)->sin_port = htons(ps5 ? CHIAKI_DISCOVERY_PORT_PS5 : CHIAKI_DISCOVERY_PORT_PS4); ChiakiDiscoveryPacket packet = { 0 }; packet.cmd = CHIAKI_DISCOVERY_CMD_WAKEUP; + packet.protocol_version = ps5 ? CHIAKI_DISCOVERY_PROTOCOL_VERSION_PS5 : CHIAKI_DISCOVERY_PROTOCOL_VERSION_PS4; packet.user_credential = user_credential; ChiakiErrorCode err; diff --git a/lib/src/discoveryservice.c b/lib/src/discoveryservice.c index 5ec84e6..463c65f 100644 --- a/lib/src/discoveryservice.c +++ b/lib/src/discoveryservice.c @@ -135,9 +135,27 @@ static void discovery_service_ping(ChiakiDiscoveryService *service) CHIAKI_LOGV(service->log, "Discovery Service sending ping"); ChiakiDiscoveryPacket packet = { 0 }; packet.cmd = CHIAKI_DISCOVERY_CMD_SRCH; + packet.protocol_version = CHIAKI_DISCOVERY_PROTOCOL_VERSION_PS4; + if(service->options.send_addr->sa_family == AF_INET) + ((struct sockaddr_in *)service->options.send_addr)->sin_port = htons(CHIAKI_DISCOVERY_PORT_PS4); + else if(service->options.send_addr->sa_family == AF_INET6) + ((struct sockaddr_in6 *)service->options.send_addr)->sin6_port = htons(CHIAKI_DISCOVERY_PORT_PS4); + else + { + CHIAKI_LOGE(service->log, "Discovery Service send_addr has unknown sa_family"); + return; + } err = chiaki_discovery_send(&service->discovery, &packet, service->options.send_addr, service->options.send_addr_size); if(err != CHIAKI_ERR_SUCCESS) - CHIAKI_LOGE(service->log, "Discovery Service failed to send ping"); + CHIAKI_LOGE(service->log, "Discovery Service failed to send ping for PS4"); + packet.protocol_version = CHIAKI_DISCOVERY_PROTOCOL_VERSION_PS5; + if(service->options.send_addr->sa_family == AF_INET) + ((struct sockaddr_in *)service->options.send_addr)->sin_port = htons(CHIAKI_DISCOVERY_PORT_PS5); + else // if(service->options.send_addr->sa_family == AF_INET6) + ((struct sockaddr_in6 *)service->options.send_addr)->sin6_port = htons(CHIAKI_DISCOVERY_PORT_PS5); + err = chiaki_discovery_send(&service->discovery, &packet, service->options.send_addr, service->options.send_addr_size); + if(err != CHIAKI_ERR_SUCCESS) + CHIAKI_LOGE(service->log, "Discovery Service failed to send ping for PS5"); } static void discovery_service_drop_old_hosts(ChiakiDiscoveryService *service) @@ -264,4 +282,4 @@ static void discovery_service_report_state(ChiakiDiscoveryService *service) // service->state_mutex must be locked if(service->options.cb) service->options.cb(service->hosts, service->hosts_count, service->options.cb_user); -} \ No newline at end of file +} diff --git a/switch/src/discoverymanager.cpp b/switch/src/discoverymanager.cpp index 4c0ebbe..0b8bb4d 100644 --- a/switch/src/discoverymanager.cpp +++ b/switch/src/discoverymanager.cpp @@ -63,7 +63,6 @@ void DiscoveryManager::SetService(bool enable) sockaddr_in addr = {}; addr.sin_family = AF_INET; - addr.sin_port = htons(CHIAKI_DISCOVERY_PORT); addr.sin_addr.s_addr = GetIPv4BroadcastAddr(); options.send_addr = reinterpret_cast(&addr); options.send_addr_size = sizeof(addr); From 767e545f381f66b4a591ce2d4d579090a49404b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Fri, 25 Dec 2020 15:04:41 +0100 Subject: [PATCH 102/237] Fix Version Retry --- lib/include/chiaki/common.h | 3 ++- lib/src/session.c | 29 ++++++++++++++++++----------- 2 files changed, 20 insertions(+), 12 deletions(-) diff --git a/lib/include/chiaki/common.h b/lib/include/chiaki/common.h index b28f696..b035c9d 100644 --- a/lib/include/chiaki/common.h +++ b/lib/include/chiaki/common.h @@ -50,7 +50,8 @@ typedef enum CHIAKI_ERR_INVALID_RESPONSE, CHIAKI_ERR_INVALID_MAC, CHIAKI_ERR_UNINITIALIZED, - CHIAKI_ERR_FEC_FAILED + CHIAKI_ERR_FEC_FAILED, + CHIAKI_ERR_VERSION_MISMATCH } ChiakiErrorCode; CHIAKI_EXPORT const char *chiaki_error_string(ChiakiErrorCode code); diff --git a/lib/src/session.c b/lib/src/session.c index 8a0d06e..74e821a 100644 --- a/lib/src/session.c +++ b/lib/src/session.c @@ -349,7 +349,6 @@ static bool session_check_state_pred_pin(void *user) static void *session_thread_func(void *arg) { ChiakiSession *session = arg; - bool success; chiaki_mutex_lock(&session->state_mutex); @@ -369,24 +368,27 @@ static void *session_thread_func(void *arg) CHIAKI_LOGI(session->log, "Starting session request for %s", session->connect_info.ps5 ? "PS5" : "PS4"); ChiakiTarget server_target = CHIAKI_TARGET_PS4_UNKNOWN; - success = session_thread_request_session(session, &server_target) == CHIAKI_ERR_SUCCESS; - // TODO: Check Network error return + ChiakiErrorCode err = session_thread_request_session(session, &server_target); - if(!success && chiaki_target_is_unknown(server_target)) + if(err == CHIAKI_ERR_VERSION_MISMATCH && !chiaki_target_is_unknown(server_target)) { CHIAKI_LOGI(session->log, "Attempting to re-request session with Server's RP-Version"); session->target = server_target; - success = session_thread_request_session(session, &server_target) == CHIAKI_ERR_SUCCESS; + err = session_thread_request_session(session, &server_target); } + else if(err != CHIAKI_ERR_SUCCESS) + QUIT(quit); - if(!success && chiaki_target_is_unknown(server_target)) + if(err == CHIAKI_ERR_VERSION_MISMATCH && !chiaki_target_is_unknown(server_target)) { CHIAKI_LOGI(session->log, "Attempting to re-request session even harder with Server's RP-Version!!!"); session->target = server_target; - success = session_thread_request_session(session, NULL) == CHIAKI_ERR_SUCCESS; + err = session_thread_request_session(session, NULL); } + else if(err != CHIAKI_ERR_SUCCESS) + QUIT(quit); - if(!success) + if(err != CHIAKI_ERR_SUCCESS) QUIT(quit); CHIAKI_LOGI(session->log, "Session request successful"); @@ -398,7 +400,7 @@ static void *session_thread_func(void *arg) CHIAKI_LOGI(session->log, "Starting ctrl"); - ChiakiErrorCode err = chiaki_ctrl_start(&session->ctrl); + err = chiaki_ctrl_start(&session->ctrl); if(err != CHIAKI_ERR_SUCCESS) QUIT(quit); @@ -760,6 +762,7 @@ static ChiakiErrorCode session_thread_request_session(ChiakiSession *session, Ch SessionResponse response; parse_session_response(&response, &http_response); + ChiakiErrorCode r = CHIAKI_ERR_UNKNOWN; if(response.success) { size_t nonce_len = CHIAKI_RPCRYPT_KEY_SIZE; @@ -770,6 +773,8 @@ static ChiakiErrorCode session_thread_request_session(ChiakiSession *session, Ch response.success = false; session->quit_reason = CHIAKI_QUIT_REASON_SESSION_REQUEST_UNKNOWN; } + else + r = CHIAKI_ERR_SUCCESS; } else if((response.error_code == CHIAKI_RP_APPLICATION_REASON_RP_VERSION || response.error_code == CHIAKI_RP_APPLICATION_REASON_UNKNOWN) @@ -778,7 +783,7 @@ static ChiakiErrorCode session_thread_request_session(ChiakiSession *session, Ch CHIAKI_LOGI(session->log, "Reported RP-Version mismatch. ours = %s, server = %s", rp_version_str ? rp_version_str : "", response.rp_version); *target_out = chiaki_rp_version_parse(response.rp_version, session->connect_info.ps5); - if(*target_out != CHIAKI_TARGET_PS4_UNKNOWN) + if(!chiaki_target_is_unknown(*target_out)) CHIAKI_LOGI(session->log, "Detected Server RP-Version %s", chiaki_rp_version_string(*target_out)); else if(!strcmp(response.rp_version, "5.0")) { @@ -790,6 +795,7 @@ static ChiakiErrorCode session_thread_request_session(ChiakiSession *session, Ch CHIAKI_LOGE(session->log, "Server RP-Version is unknown"); session->quit_reason = CHIAKI_QUIT_REASON_SESSION_REQUEST_RP_VERSION_MISMATCH; } + r = CHIAKI_ERR_VERSION_MISMATCH; } else { @@ -804,6 +810,7 @@ static ChiakiErrorCode session_thread_request_session(ChiakiSession *session, Ch break; case CHIAKI_RP_APPLICATION_REASON_RP_VERSION: session->quit_reason = CHIAKI_QUIT_REASON_SESSION_REQUEST_RP_VERSION_MISMATCH; + r = CHIAKI_ERR_VERSION_MISMATCH; break; default: session->quit_reason = CHIAKI_QUIT_REASON_SESSION_REQUEST_UNKNOWN; @@ -813,7 +820,7 @@ static ChiakiErrorCode session_thread_request_session(ChiakiSession *session, Ch chiaki_http_response_fini(&http_response); CHIAKI_SOCKET_CLOSE(session_sock); - return response.success ? CHIAKI_ERR_SUCCESS : CHIAKI_ERR_UNKNOWN; + return r; } CHIAKI_EXPORT ChiakiErrorCode chiaki_session_goto_bed(ChiakiSession *session) From aba17d48a32ba42b0d14f298143b1f2a88cada25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Fri, 25 Dec 2020 15:04:57 +0100 Subject: [PATCH 103/237] Add correct Bitrate for 1080p --- gui/src/settingsdialog.cpp | 2 +- lib/src/session.c | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/gui/src/settingsdialog.cpp b/gui/src/settingsdialog.cpp index 4411343..41b267e 100644 --- a/gui/src/settingsdialog.cpp +++ b/gui/src/settingsdialog.cpp @@ -139,7 +139,7 @@ SettingsDialog::SettingsDialog(Settings *settings, QWidget *parent) : QDialog(pa { CHIAKI_VIDEO_RESOLUTION_PRESET_360p, "360p"}, { CHIAKI_VIDEO_RESOLUTION_PRESET_540p, "540p"}, { CHIAKI_VIDEO_RESOLUTION_PRESET_720p, "720p"}, - { CHIAKI_VIDEO_RESOLUTION_PRESET_1080p, "1080p (PS4 Pro only)"} + { CHIAKI_VIDEO_RESOLUTION_PRESET_1080p, "1080p (PS5 and PS4 Pro only)"} }; auto current_res = settings->GetResolution(); for(const auto &p : resolution_strings) diff --git a/lib/src/session.c b/lib/src/session.c index 74e821a..84177ed 100644 --- a/lib/src/session.c +++ b/lib/src/session.c @@ -108,7 +108,7 @@ CHIAKI_EXPORT void chiaki_connect_video_profile_preset(ChiakiConnectVideoProfile case CHIAKI_VIDEO_RESOLUTION_PRESET_1080p: profile->width = 1920; profile->height = 1080; - profile->bitrate = 10000; // TODO + profile->bitrate = 15000; break; default: profile->width = 0; From 673a2de9c120c71fc54f48c8f837dd55e664ee10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Fri, 25 Dec 2020 17:58:56 +0100 Subject: [PATCH 104/237] Move FFMPEG Decoder to lib --- CMakeLists.txt | 25 +++- gui/CMakeLists.txt | 8 +- gui/include/avopenglframeuploader.h | 10 +- gui/include/avopenglwidget.h | 6 +- gui/include/logsniffer.h | 81 +++++++++++ gui/include/settings.h | 5 +- gui/include/settingsdialog.h | 2 +- gui/include/streamsession.h | 13 +- gui/include/videodecoder.h | 77 ---------- gui/src/avopenglframeuploader.cpp | 23 +-- gui/src/avopenglwidget.cpp | 19 +-- gui/src/main.cpp | 1 - gui/src/settings.cpp | 23 ++- gui/src/settingsdialog.cpp | 34 ++--- gui/src/streamsession.cpp | 58 +++++--- gui/src/streamwindow.cpp | 4 +- gui/src/videodecoder.cpp | 177 ----------------------- lib/CMakeLists.txt | 10 ++ lib/include/chiaki/common.h | 2 + lib/include/chiaki/ffmpegdecoder.h | 43 ++++++ lib/include/chiaki/log.h | 14 ++ lib/src/common.c | 15 ++ lib/src/ffmpegdecoder.c | 216 ++++++++++++++++++++++++++++ lib/src/log.c | 50 ++++++- 24 files changed, 566 insertions(+), 350 deletions(-) create mode 100644 gui/include/logsniffer.h delete mode 100644 gui/include/videodecoder.h delete mode 100644 gui/src/videodecoder.cpp create mode 100644 lib/include/chiaki/ffmpegdecoder.h create mode 100644 lib/src/ffmpegdecoder.c diff --git a/CMakeLists.txt b/CMakeLists.txt index 67303ed..78662a4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -16,6 +16,12 @@ option(CHIAKI_ENABLE_ANDROID "Enable Android (Use only as part of the Gradle Pro option(CHIAKI_ENABLE_BOREALIS "Enable Borealis GUI (For Nintendo Switch or PC)" OFF) tri_option(CHIAKI_ENABLE_SETSU "Enable libsetsu for touchpad input from controller" AUTO) option(CHIAKI_LIB_ENABLE_OPUS "Use Opus as part of Chiaki Lib" ON) +if(CHIAKI_ENABLE_GUI OR CHIAKI_ENABLE_BOREALIS) + set(CHIAKI_FFMPEG_DEFAULT ON) +else() + set(CHIAKI_FFMPEG_DEFAULT AUTO) +endif() +tri_option(CHIAKI_ENABLE_FFMPEG_DECODER "Enable FFMPEG video decoder" ${CHIAKI_FFMPEG_DEFAULT}) tri_option(CHIAKI_ENABLE_PI_DECODER "Enable Raspberry Pi-specific video decoder (requires libraspberrypi0 and libraspberrypi-doc)" AUTO) option(CHIAKI_LIB_ENABLE_MBEDTLS "Use mbedtls instead of OpenSSL as part of Chiaki Lib" OFF) option(CHIAKI_LIB_OPENSSL_EXTERNAL_PROJECT "Use OpenSSL as CMake external project" OFF) @@ -85,6 +91,24 @@ if(CHIAKI_LIB_ENABLE_MBEDTLS) add_definitions(-DCHIAKI_LIB_ENABLE_MBEDTLS) endif() +if(CHIAKI_ENABLE_FFMPEG_DECODER) + find_package(FFMPEG COMPONENTS avcodec avutil) + if(FFMPEG_FOUND) + set(CHIAKI_ENABLE_FFMPEG_DECODER ON) + else() + if(NOT CHIAKI_ENABLE_FFMPEG_DECODER STREQUAL AUTO) + message(FATAL_ERROR "CHIAKI_ENABLE_FFMPEG_DECODER is set to ON, but ffmpeg could not be found.") + endif() + set(CHIAKI_ENABLE_FFMPEG_DECODER OFF) + endif() +endif() + +if(CHIAKI_ENABLE_FFMPEG_DECODER) + message(STATUS "FFMPEG Decoder enabled") +else() + message(STATUS "FFMPEG Decoder disabled") +endif() + if(CHIAKI_ENABLE_PI_DECODER) find_package(ILClient) if(ILClient_FOUND) @@ -105,7 +129,6 @@ else() message(STATUS "Pi Decoder disabled") endif() - add_subdirectory(lib) if(CHIAKI_ENABLE_CLI) diff --git a/gui/CMakeLists.txt b/gui/CMakeLists.txt index a61b763..5f44908 100644 --- a/gui/CMakeLists.txt +++ b/gui/CMakeLists.txt @@ -14,8 +14,6 @@ if(WIN32) add_definitions(-DWIN32_LEAN_AND_MEAN) endif() -find_package(FFMPEG REQUIRED COMPONENTS avcodec avutil) - set(RESOURCE_FILES "") if(APPLE) @@ -28,8 +26,6 @@ add_executable(chiaki WIN32 src/main.cpp include/streamwindow.h src/streamwindow.cpp - include/videodecoder.h - src/videodecoder.cpp include/mainwindow.h src/mainwindow.cpp include/dynamicgridwidget.h @@ -73,7 +69,6 @@ if(CHIAKI_ENABLE_CLI) target_link_libraries(chiaki chiaki-cli-lib) endif() -target_link_libraries(chiaki FFMPEG::avcodec FFMPEG::avutil) target_link_libraries(chiaki Qt5::Core Qt5::Widgets Qt5::Gui Qt5::Concurrent Qt5::Multimedia Qt5::OpenGL Qt5::Svg) if(APPLE) target_link_libraries(chiaki Qt5::MacExtras) @@ -87,6 +82,9 @@ if(CHIAKI_ENABLE_SETSU) target_link_libraries(chiaki setsu) target_compile_definitions(chiaki PRIVATE CHIAKI_GUI_ENABLE_SETSU) endif() +if(NOT CHIAKI_ENABLE_FFMPEG_DECODER) + message(FATAL_ERROR "Chiaki GUI requires CHIAKI_ENABLE_FFMPEG_DECODER=ON") +endif() set_target_properties(chiaki PROPERTIES MACOSX_BUNDLE TRUE diff --git a/gui/include/avopenglframeuploader.h b/gui/include/avopenglframeuploader.h index 51fd2d9..1e37054 100644 --- a/gui/include/avopenglframeuploader.h +++ b/gui/include/avopenglframeuploader.h @@ -6,8 +6,10 @@ #include #include +#include + +class StreamSession; class AVOpenGLWidget; -class VideoDecoder; class QSurface; class AVOpenGLFrameUploader: public QObject @@ -15,16 +17,16 @@ class AVOpenGLFrameUploader: public QObject Q_OBJECT private: - VideoDecoder *decoder; + StreamSession *session; AVOpenGLWidget *widget; QOpenGLContext *context; QSurface *surface; private slots: - void UpdateFrame(); + void UpdateFrameFromDecoder(); public: - AVOpenGLFrameUploader(VideoDecoder *decoder, AVOpenGLWidget *widget, QOpenGLContext *context, QSurface *surface); + AVOpenGLFrameUploader(StreamSession *session, AVOpenGLWidget *widget, QOpenGLContext *context, QSurface *surface); }; #endif // CHIAKI_AVOPENGLFRAMEUPLOADER_H diff --git a/gui/include/avopenglwidget.h b/gui/include/avopenglwidget.h index 328b74f..6f234e9 100644 --- a/gui/include/avopenglwidget.h +++ b/gui/include/avopenglwidget.h @@ -15,7 +15,7 @@ extern "C" #define MAX_PANES 3 -class VideoDecoder; +class StreamSession; class AVOpenGLFrameUploader; class QOffscreenSurface; @@ -53,7 +53,7 @@ class AVOpenGLWidget: public QOpenGLWidget Q_OBJECT private: - VideoDecoder *decoder; + StreamSession *session; GLuint program; GLuint vbo; @@ -74,7 +74,7 @@ class AVOpenGLWidget: public QOpenGLWidget public: static QSurfaceFormat CreateSurfaceFormat(); - explicit AVOpenGLWidget(VideoDecoder *decoder, QWidget *parent = nullptr); + explicit AVOpenGLWidget(StreamSession *session, QWidget *parent = nullptr); ~AVOpenGLWidget() override; void SwapFrames(); diff --git a/gui/include/logsniffer.h b/gui/include/logsniffer.h new file mode 100644 index 0000000..aa398a0 --- /dev/null +++ b/gui/include/logsniffer.h @@ -0,0 +1,81 @@ +// SPDX-License-Identifier: LicenseRef-GPL-3.0-or-later-OpenSSL + +#ifndef CHIAKI_LOG_SNIFFER_H +#define CHIAKI_LOG_SNIFFER_H + +#include + +#include +#include +#include +#include + +#ifdef CHIAKI_GUI_ENABLE_SDL_GAMECONTROLLER +#include +#endif + +class Controller; + +class ControllerManager : public QObject +{ + Q_OBJECT + + friend class Controller; + + private: +#ifdef CHIAKI_GUI_ENABLE_SDL_GAMECONTROLLER + QSet available_controllers; +#endif + QMap open_controllers; + + void ControllerClosed(Controller *controller); + + private slots: + void UpdateAvailableControllers(); + void HandleEvents(); + void ControllerEvent(int device_id); + + public: + static ControllerManager *GetInstance(); + + ControllerManager(QObject *parent = nullptr); + ~ControllerManager(); + + QSet GetAvailableControllers(); + Controller *OpenController(int device_id); + + signals: + void AvailableControllersUpdated(); +}; + +class Controller : public QObject +{ + Q_OBJECT + + friend class ControllerManager; + + private: + Controller(int device_id, ControllerManager *manager); + + void UpdateState(); + + ControllerManager *manager; + int id; + +#ifdef CHIAKI_GUI_ENABLE_SDL_GAMECONTROLLER + SDL_GameController *controller; +#endif + + public: + ~Controller(); + + bool IsConnected(); + int GetDeviceID(); + QString GetName(); + ChiakiControllerState GetState(); + + signals: + void StateChanged(); +}; + +#endif // CHIAKI_CONTROLLERMANAGER_H diff --git a/gui/include/settings.h b/gui/include/settings.h index c41ef0b..dc8db49 100644 --- a/gui/include/settings.h +++ b/gui/include/settings.h @@ -6,7 +6,6 @@ #include #include "host.h" -#include "videodecoder.h" #include #include @@ -79,8 +78,8 @@ class Settings : public QObject Decoder GetDecoder() const; void SetDecoder(Decoder decoder); - HardwareDecodeEngine GetHardwareDecodeEngine() const; - void SetHardwareDecodeEngine(HardwareDecodeEngine enabled); + QString GetHardwareDecoder() const; + void SetHardwareDecoder(const QString &hw_decoder); unsigned int GetAudioBufferSizeDefault() const; diff --git a/gui/include/settingsdialog.h b/gui/include/settingsdialog.h index b639b53..5b741dd 100644 --- a/gui/include/settingsdialog.h +++ b/gui/include/settingsdialog.h @@ -27,7 +27,7 @@ class SettingsDialog : public QDialog QLineEdit *audio_buffer_size_edit; QComboBox *audio_device_combo_box; QCheckBox *pi_decoder_check_box; - QComboBox *hardware_decode_combo_box; + QComboBox *hw_decoder_combo_box; QListWidget *registered_hosts_list_widget; QPushButton *delete_registered_host_button; diff --git a/gui/include/streamsession.h b/gui/include/streamsession.h index aad26a3..c98758e 100644 --- a/gui/include/streamsession.h +++ b/gui/include/streamsession.h @@ -5,6 +5,7 @@ #include #include +#include #if CHIAKI_LIB_ENABLE_PI_DECODER #include @@ -14,7 +15,6 @@ #include #endif -#include "videodecoder.h" #include "exception.h" #include "sessionlog.h" #include "controllermanager.h" @@ -41,7 +41,7 @@ struct StreamSessionConnectInfo Settings *settings; QMap key_map; Decoder decoder; - HardwareDecodeEngine hw_decode_engine; + QString hw_decoder; QString audio_out_device; uint32_t log_level_mask; QString log_file; @@ -78,7 +78,8 @@ class StreamSession : public QObject ChiakiControllerState keyboard_state; - VideoDecoder *video_decoder; + ChiakiFfmpegDecoder *ffmpeg_decoder; + void TriggerFfmpegFrameAvailable(); #if CHIAKI_LIB_ENABLE_PI_DECODER ChiakiPiDecoder *pi_decoder; #endif @@ -91,7 +92,6 @@ class StreamSession : public QObject QMap key_map; void PushAudioFrame(int16_t *buf, size_t samples_count); - void PushVideoSample(uint8_t *buf, size_t buf_size); void Event(ChiakiEvent *event); #if CHIAKI_GUI_ENABLE_SETSU void HandleSetsuEvent(SetsuEvent *event); @@ -112,8 +112,9 @@ class StreamSession : public QObject void SetLoginPIN(const QString &pin); + ChiakiLog *GetChiakiLog() { return log.GetChiakiLog(); } QList GetControllers() { return controllers.values(); } - VideoDecoder *GetVideoDecoder() { return video_decoder; } + ChiakiFfmpegDecoder *GetFfmpegDecoder() { return ffmpeg_decoder; } #if CHIAKI_LIB_ENABLE_PI_DECODER ChiakiPiDecoder *GetPiDecoder() { return pi_decoder; } #endif @@ -122,7 +123,7 @@ class StreamSession : public QObject void HandleMouseEvent(QMouseEvent *event); signals: - void CurrentImageUpdated(); + void FfmpegFrameAvailable(); void SessionQuit(ChiakiQuitReason reason, const QString &reason_str); void LoginPINRequested(bool incorrect); diff --git a/gui/include/videodecoder.h b/gui/include/videodecoder.h deleted file mode 100644 index f13278a..0000000 --- a/gui/include/videodecoder.h +++ /dev/null @@ -1,77 +0,0 @@ -// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL - -#ifndef CHIAKI_VIDEODECODER_H -#define CHIAKI_VIDEODECODER_H - -#include - -#include "exception.h" - -#include -#include -#include - -extern "C" -{ -#include -} - -#include - - -typedef enum { - HW_DECODE_NONE = 0, - HW_DECODE_VAAPI = 1, - HW_DECODE_VDPAU = 2, - HW_DECODE_VIDEOTOOLBOX = 3, - HW_DECODE_CUDA = 4, -} HardwareDecodeEngine; - - -static const QMap hardware_decode_engine_names = { - { HW_DECODE_NONE, "none"}, - { HW_DECODE_VAAPI, "vaapi"}, - { HW_DECODE_VDPAU, "vdpau"}, - { HW_DECODE_VIDEOTOOLBOX, "videotoolbox"}, - { HW_DECODE_CUDA, "cuda"}, -}; - -class VideoDecoderException: public Exception -{ - public: - explicit VideoDecoderException(const QString &msg) : Exception(msg) {}; -}; - -class VideoDecoder: public QObject -{ - Q_OBJECT - - public: - VideoDecoder(HardwareDecodeEngine hw_decode_engine, ChiakiLog *log); - ~VideoDecoder(); - - void PushFrame(uint8_t *buf, size_t buf_size); - AVFrame *PullFrame(); - AVFrame *GetFromHardware(AVFrame *hw_frame); - - ChiakiLog *GetChiakiLog() { return log; } - - enum AVPixelFormat PixelFormat() { return hw_decode_engine?AV_PIX_FMT_NV12:AV_PIX_FMT_YUV420P; } - - signals: - void FramesAvailable(); - - private: - HardwareDecodeEngine hw_decode_engine; - - ChiakiLog *log; - QMutex mutex; - - AVCodec *codec; - AVCodecContext *codec_context; - - enum AVPixelFormat hw_pix_fmt; - AVBufferRef *hw_device_ctx; -}; - -#endif // CHIAKI_VIDEODECODER_H diff --git a/gui/src/avopenglframeuploader.cpp b/gui/src/avopenglframeuploader.cpp index c57da72..fd68a74 100644 --- a/gui/src/avopenglframeuploader.cpp +++ b/gui/src/avopenglframeuploader.cpp @@ -2,33 +2,40 @@ #include #include -#include +#include #include #include -AVOpenGLFrameUploader::AVOpenGLFrameUploader(VideoDecoder *decoder, AVOpenGLWidget *widget, QOpenGLContext *context, QSurface *surface) +AVOpenGLFrameUploader::AVOpenGLFrameUploader(StreamSession *session, AVOpenGLWidget *widget, QOpenGLContext *context, QSurface *surface) : QObject(nullptr), - decoder(decoder), + session(session), widget(widget), context(context), surface(surface) { - connect(decoder, SIGNAL(FramesAvailable()), this, SLOT(UpdateFrame())); + connect(session, &StreamSession::FfmpegFrameAvailable, this, &AVOpenGLFrameUploader::UpdateFrameFromDecoder); } -void AVOpenGLFrameUploader::UpdateFrame() +void AVOpenGLFrameUploader::UpdateFrameFromDecoder() { + ChiakiFfmpegDecoder *decoder = session->GetFfmpegDecoder(); + if(!decoder) + { + CHIAKI_LOGE(session->GetChiakiLog(), "Session has no ffmpeg decoder"); + return; + } + if(QOpenGLContext::currentContext() != context) context->makeCurrent(surface); - AVFrame *next_frame = decoder->PullFrame(); + AVFrame *next_frame = chiaki_ffmpeg_decoder_pull_frame(decoder); if(!next_frame) return; - bool success = widget->GetBackgroundFrame()->Update(next_frame, decoder->GetChiakiLog()); + bool success = widget->GetBackgroundFrame()->Update(next_frame, decoder->log); av_frame_free(&next_frame); if(success) widget->SwapFrames(); -} \ No newline at end of file +} diff --git a/gui/src/avopenglwidget.cpp b/gui/src/avopenglwidget.cpp index 1aa526a..3d1d8eb 100644 --- a/gui/src/avopenglwidget.cpp +++ b/gui/src/avopenglwidget.cpp @@ -1,8 +1,8 @@ // SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL #include -#include #include +#include #include #include @@ -123,14 +123,15 @@ QSurfaceFormat AVOpenGLWidget::CreateSurfaceFormat() return format; } -AVOpenGLWidget::AVOpenGLWidget(VideoDecoder *decoder, QWidget *parent) +AVOpenGLWidget::AVOpenGLWidget(StreamSession *session, QWidget *parent) : QOpenGLWidget(parent), - decoder(decoder) + session(session) { + enum AVPixelFormat pixel_format = chiaki_ffmpeg_decoder_get_pixel_format(session->GetFfmpegDecoder()); conversion_config = nullptr; for(auto &cc: conversion_configs) { - if(decoder->PixelFormat() == cc.pixel_format) + if(pixel_format == cc.pixel_format) { conversion_config = &cc; break; @@ -245,7 +246,7 @@ void AVOpenGLWidget::initializeGL() auto f = QOpenGLContext::currentContext()->extraFunctions(); const char *gl_version = (const char *)f->glGetString(GL_VERSION); - CHIAKI_LOGI(decoder->GetChiakiLog(), "OpenGL initialized with version \"%s\"", gl_version ? gl_version : "(null)"); + CHIAKI_LOGI(session->GetChiakiLog(), "OpenGL initialized with version \"%s\"", gl_version ? gl_version : "(null)"); #ifdef DEBUG_OPENGL auto logger = new QOpenGLDebugLogger(this); @@ -266,7 +267,7 @@ void AVOpenGLWidget::initializeGL() QVector info_log(info_log_size); f->glGetShaderInfoLog(shader, info_log_size, &info_log_size, info_log.data()); f->glDeleteShader(shader); - CHIAKI_LOGE(decoder->GetChiakiLog(), "Failed to Compile Shader:\n%s", info_log.data()); + CHIAKI_LOGE(session->GetChiakiLog(), "Failed to Compile Shader:\n%s", info_log.data()); }; GLuint shader_vert = f->glCreateShader(GL_VERTEX_SHADER); @@ -294,7 +295,7 @@ void AVOpenGLWidget::initializeGL() QVector info_log(info_log_size); f->glGetProgramInfoLog(program, info_log_size, &info_log_size, info_log.data()); f->glDeleteProgram(program); - CHIAKI_LOGE(decoder->GetChiakiLog(), "Failed to Link Shader Program:\n%s", info_log.data()); + CHIAKI_LOGE(session->GetChiakiLog(), "Failed to Link Shader Program:\n%s", info_log.data()); return; } @@ -346,14 +347,14 @@ void AVOpenGLWidget::initializeGL() frame_uploader_context->setShareContext(context()); if(!frame_uploader_context->create()) { - CHIAKI_LOGE(decoder->GetChiakiLog(), "Failed to create upload OpenGL context"); + CHIAKI_LOGE(session->GetChiakiLog(), "Failed to create upload OpenGL context"); return; } frame_uploader_surface = new QOffscreenSurface(); frame_uploader_surface->setFormat(context()->format()); frame_uploader_surface->create(); - frame_uploader = new AVOpenGLFrameUploader(decoder, this, frame_uploader_context, frame_uploader_surface); + frame_uploader = new AVOpenGLFrameUploader(session, this, frame_uploader_context, frame_uploader_surface); frame_fg = 0; frame_uploader_thread = new QThread(this); diff --git a/gui/src/main.cpp b/gui/src/main.cpp index e4fad7c..56a85f2 100644 --- a/gui/src/main.cpp +++ b/gui/src/main.cpp @@ -4,7 +4,6 @@ int real_main(int argc, char *argv[]); int main(int argc, char *argv[]) { return real_main(argc, argv); } #include -#include #include #include #include diff --git a/gui/src/settings.cpp b/gui/src/settings.cpp index 3f637bb..c8c2a9d 100644 --- a/gui/src/settings.cpp +++ b/gui/src/settings.cpp @@ -37,6 +37,10 @@ static void MigrateSettingsTo2(QSettings *settings) i++; } settings->endArray(); + QString hw_decoder = settings->value("settings/hw_decode_engine").toString(); + settings->remove("settings/hw_decode_engine"); + if(hw_decoder != "none") + settings->setValue("settings/hw_decoder", hw_decoder); } static void MigrateSettings(QSettings *settings) @@ -160,25 +164,14 @@ void Settings::SetDecoder(Decoder decoder) settings.setValue("settings/decoder", decoder_values[decoder]); } -static const QMap hw_decode_engine_values = { - { HW_DECODE_NONE, "none" }, - { HW_DECODE_VAAPI, "vaapi" }, - { HW_DECODE_VDPAU, "vdpau" }, - { HW_DECODE_VIDEOTOOLBOX, "videotoolbox" }, - { HW_DECODE_CUDA, "cuda" } -}; - -static const HardwareDecodeEngine hw_decode_engine_default = HW_DECODE_NONE; - -HardwareDecodeEngine Settings::GetHardwareDecodeEngine() const +QString Settings::GetHardwareDecoder() const { - auto v = settings.value("settings/hw_decode_engine", hw_decode_engine_values[hw_decode_engine_default]).toString(); - return hw_decode_engine_values.key(v, hw_decode_engine_default); + return settings.value("settings/hw_decoder").toString(); } -void Settings::SetHardwareDecodeEngine(HardwareDecodeEngine engine) +void Settings::SetHardwareDecoder(const QString &hw_decoder) { - settings.setValue("settings/hw_decode_engine", hw_decode_engine_values[engine]); + settings.setValue("settings/hw_decoder", hw_decoder); } unsigned int Settings::GetAudioBufferSize() const diff --git a/gui/src/settingsdialog.cpp b/gui/src/settingsdialog.cpp index 41b267e..6eb9d98 100644 --- a/gui/src/settingsdialog.cpp +++ b/gui/src/settingsdialog.cpp @@ -5,7 +5,6 @@ #include #include #include -#include #include #include @@ -23,6 +22,7 @@ #include #include +#include const char * const about_string = "

Chiaki

by thestr4ng3r, version " CHIAKI_VERSION @@ -202,22 +202,22 @@ SettingsDialog::SettingsDialog(Settings *settings, QWidget *parent) : QDialog(pa pi_decoder_check_box = nullptr; #endif - hardware_decode_combo_box = new QComboBox(this); - static const QList> hardware_decode_engines = { - { HW_DECODE_NONE, "none"}, - { HW_DECODE_VAAPI, "vaapi"}, - { HW_DECODE_VIDEOTOOLBOX, "videotoolbox"}, - { HW_DECODE_CUDA, "cuda"} - }; - auto current_hardware_decode_engine = settings->GetHardwareDecodeEngine(); - for(const auto &p : hardware_decode_engines) + hw_decoder_combo_box = new QComboBox(this); + hw_decoder_combo_box->addItem("none", QString()); + auto current_hw_decoder = settings->GetHardwareDecoder(); + enum AVHWDeviceType hw_dev = AV_HWDEVICE_TYPE_NONE; + while(true) { - hardware_decode_combo_box->addItem(p.second, (int)p.first); - if(current_hardware_decode_engine == p.first) - hardware_decode_combo_box->setCurrentIndex(hardware_decode_combo_box->count() - 1); + hw_dev = av_hwdevice_iterate_types(hw_dev); + if(hw_dev == AV_HWDEVICE_TYPE_NONE) + break; + QString name = QString::fromUtf8(av_hwdevice_get_type_name(hw_dev)); + hw_decoder_combo_box->addItem(name, name); + if(current_hw_decoder == name) + hw_decoder_combo_box->setCurrentIndex(hw_decoder_combo_box->count() - 1); } - connect(hardware_decode_combo_box, SIGNAL(currentIndexChanged(int)), this, SLOT(HardwareDecodeEngineSelected())); - decode_settings_layout->addRow(tr("Hardware decode method:"), hardware_decode_combo_box); + connect(hw_decoder_combo_box, SIGNAL(currentIndexChanged(int)), this, SLOT(HardwareDecodeEngineSelected())); + decode_settings_layout->addRow(tr("Hardware decode method:"), hw_decoder_combo_box); UpdateHardwareDecodeEngineComboBox(); // Registered Consoles @@ -329,12 +329,12 @@ void SettingsDialog::AudioOutputSelected() void SettingsDialog::HardwareDecodeEngineSelected() { - settings->SetHardwareDecodeEngine((HardwareDecodeEngine)hardware_decode_combo_box->currentData().toInt()); + settings->SetHardwareDecoder(hw_decoder_combo_box->currentData().toString()); } void SettingsDialog::UpdateHardwareDecodeEngineComboBox() { - hardware_decode_combo_box->setEnabled(settings->GetDecoder() == Decoder::Ffmpeg); + hw_decoder_combo_box->setEnabled(settings->GetDecoder() == Decoder::Ffmpeg); } void SettingsDialog::UpdateBitratePlaceholder() diff --git a/gui/src/streamsession.cpp b/gui/src/streamsession.cpp index 396eb0b..fa696be 100644 --- a/gui/src/streamsession.cpp +++ b/gui/src/streamsession.cpp @@ -19,7 +19,7 @@ StreamSessionConnectInfo::StreamSessionConnectInfo(Settings *settings, ChiakiTar { key_map = settings->GetControllerMappingForDecoding(); decoder = settings->GetDecoder(); - hw_decode_engine = settings->GetHardwareDecodeEngine(); + hw_decoder = settings->GetHardwareDecoder(); audio_out_device = settings->GetAudioOutDevice(); log_level_mask = settings->GetLogLevelMask(); log_file = CreateLogFilename(); @@ -35,16 +35,16 @@ StreamSessionConnectInfo::StreamSessionConnectInfo(Settings *settings, ChiakiTar static void AudioSettingsCb(uint32_t channels, uint32_t rate, void *user); static void AudioFrameCb(int16_t *buf, size_t samples_count, void *user); -static bool VideoSampleCb(uint8_t *buf, size_t buf_size, void *user); static void EventCb(ChiakiEvent *event, void *user); #if CHIAKI_GUI_ENABLE_SETSU static void SessionSetsuCb(SetsuEvent *event, void *user); #endif +static void FfmpegFrameCb(ChiakiFfmpegDecoder *decoder, void *user); StreamSession::StreamSession(const StreamSessionConnectInfo &connect_info, QObject *parent) : QObject(parent), log(this, connect_info.log_level_mask, connect_info.log_file), - video_decoder(nullptr), + ffmpeg_decoder(nullptr), #if CHIAKI_LIB_ENABLE_PI_DECODER pi_decoder(nullptr), #endif @@ -63,7 +63,22 @@ StreamSession::StreamSession(const StreamSessionConnectInfo &connect_info, QObje else { #endif - video_decoder = new VideoDecoder(connect_info.hw_decode_engine, log.GetChiakiLog()); + ffmpeg_decoder = new ChiakiFfmpegDecoder; + ChiakiLogSniffer sniffer; + chiaki_log_sniffer_init(&sniffer, CHIAKI_LOG_ALL, GetChiakiLog()); + ChiakiErrorCode err = chiaki_ffmpeg_decoder_init(ffmpeg_decoder, + chiaki_log_sniffer_get_log(&sniffer), + connect_info.video_profile.codec, + connect_info.hw_decoder.isEmpty() ? NULL : connect_info.hw_decoder.toUtf8().constData(), + FfmpegFrameCb, this); + if(err != CHIAKI_ERR_SUCCESS) + { + QString log = QString::fromUtf8(chiaki_log_sniffer_get_buffer(&sniffer)); + chiaki_log_sniffer_fini(&sniffer); + throw ChiakiException("Failed to initialize FFMPEG Decoder:\n" + log); + } + chiaki_log_sniffer_fini(&sniffer); + ffmpeg_decoder->log = GetChiakiLog(); #if CHIAKI_LIB_ENABLE_PI_DECODER } #endif @@ -101,7 +116,7 @@ StreamSession::StreamSession(const StreamSessionConnectInfo &connect_info, QObje chiaki_controller_state_set_idle(&keyboard_state); - ChiakiErrorCode err = chiaki_session_init(&session, &chiaki_connect_info, log.GetChiakiLog()); + err = chiaki_session_init(&session, &chiaki_connect_info, GetChiakiLog()); if(err != CHIAKI_ERR_SUCCESS) throw ChiakiException("Chiaki Session Init failed: " + QString::fromLocal8Bit(chiaki_error_string(err))); @@ -116,7 +131,7 @@ StreamSession::StreamSession(const StreamSessionConnectInfo &connect_info, QObje else { #endif - chiaki_session_set_video_sample_cb(&session, VideoSampleCb, this); + chiaki_session_set_video_sample_cb(&session, chiaki_ffmpeg_decoder_video_sample_cb, ffmpeg_decoder); #if CHIAKI_LIB_ENABLE_PI_DECODER } #endif @@ -160,7 +175,11 @@ StreamSession::~StreamSession() free(pi_decoder); } #endif - delete video_decoder; + if(ffmpeg_decoder) + { + chiaki_ffmpeg_decoder_fini(ffmpeg_decoder); + delete ffmpeg_decoder; + } } void StreamSession::Start() @@ -346,11 +365,6 @@ void StreamSession::PushAudioFrame(int16_t *buf, size_t samples_count) audio_io->write((const char *)buf, static_cast(samples_count * 2 * 2)); } -void StreamSession::PushVideoSample(uint8_t *buf, size_t buf_size) -{ - video_decoder->PushFrame(buf, buf_size); -} - void StreamSession::Event(ChiakiEvent *event) { switch(event->type) @@ -431,6 +445,11 @@ void StreamSession::HandleSetsuEvent(SetsuEvent *event) } #endif +void StreamSession::TriggerFfmpegFrameAvailable() +{ + emit FfmpegFrameAvailable(); +} + class StreamSessionPrivate { public: @@ -440,11 +459,11 @@ class StreamSessionPrivate } static void PushAudioFrame(StreamSession *session, int16_t *buf, size_t samples_count) { session->PushAudioFrame(buf, samples_count); } - static void PushVideoSample(StreamSession *session, uint8_t *buf, size_t buf_size) { session->PushVideoSample(buf, buf_size); } static void Event(StreamSession *session, ChiakiEvent *event) { session->Event(event); } #if CHIAKI_GUI_ENABLE_SETSU static void HandleSetsuEvent(StreamSession *session, SetsuEvent *event) { session->HandleSetsuEvent(event); } #endif + static void TriggerFfmpegFrameAvailable(StreamSession *session) { session->TriggerFfmpegFrameAvailable(); } }; static void AudioSettingsCb(uint32_t channels, uint32_t rate, void *user) @@ -459,13 +478,6 @@ static void AudioFrameCb(int16_t *buf, size_t samples_count, void *user) StreamSessionPrivate::PushAudioFrame(session, buf, samples_count); } -static bool VideoSampleCb(uint8_t *buf, size_t buf_size, void *user) -{ - auto session = reinterpret_cast(user); - StreamSessionPrivate::PushVideoSample(session, buf, buf_size); - return true; -} - static void EventCb(ChiakiEvent *event, void *user) { auto session = reinterpret_cast(user); @@ -479,3 +491,9 @@ static void SessionSetsuCb(SetsuEvent *event, void *user) StreamSessionPrivate::HandleSetsuEvent(session, event); } #endif + +static void FfmpegFrameCb(ChiakiFfmpegDecoder *decoder, void *user) +{ + auto session = reinterpret_cast(user); + StreamSessionPrivate::TriggerFfmpegFrameAvailable(session); +} diff --git a/gui/src/streamwindow.cpp b/gui/src/streamwindow.cpp index adc686c..58f7392 100644 --- a/gui/src/streamwindow.cpp +++ b/gui/src/streamwindow.cpp @@ -47,9 +47,9 @@ void StreamWindow::Init() connect(session, &StreamSession::SessionQuit, this, &StreamWindow::SessionQuit); connect(session, &StreamSession::LoginPINRequested, this, &StreamWindow::LoginPINRequested); - if(session->GetVideoDecoder()) + if(session->GetFfmpegDecoder()) { - av_widget = new AVOpenGLWidget(session->GetVideoDecoder(), this); + av_widget = new AVOpenGLWidget(session, this); setCentralWidget(av_widget); } else diff --git a/gui/src/videodecoder.cpp b/gui/src/videodecoder.cpp deleted file mode 100644 index 48ae4e3..0000000 --- a/gui/src/videodecoder.cpp +++ /dev/null @@ -1,177 +0,0 @@ -// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL - -#include - -#include - -#include - -VideoDecoder::VideoDecoder(HardwareDecodeEngine hw_decode_engine, ChiakiLog *log) : hw_decode_engine(hw_decode_engine), log(log) -{ - enum AVHWDeviceType type; - hw_device_ctx = nullptr; - - #if LIBAVCODEC_VERSION_INT < AV_VERSION_INT(58, 10, 100) - avcodec_register_all(); - #endif - codec = avcodec_find_decoder(AV_CODEC_ID_H264); - if(!codec) - throw VideoDecoderException("H264 Codec not available"); - - codec_context = avcodec_alloc_context3(codec); - if(!codec_context) - throw VideoDecoderException("Failed to alloc codec context"); - - if(hw_decode_engine) - { - if(!hardware_decode_engine_names.contains(hw_decode_engine)) - throw VideoDecoderException("Unknown hardware decode engine!"); - - const char *hw_dec_eng = hardware_decode_engine_names[hw_decode_engine]; - CHIAKI_LOGI(log, "Using hardware decode %s", hw_dec_eng); - type = av_hwdevice_find_type_by_name(hw_dec_eng); - if (type == AV_HWDEVICE_TYPE_NONE) - throw VideoDecoderException("Can't initialize vaapi"); - - for(int i = 0;; i++) { - const AVCodecHWConfig *config = avcodec_get_hw_config(codec, i); - if(!config) - throw VideoDecoderException("avcodec_get_hw_config failed"); - if(config->methods & AV_CODEC_HW_CONFIG_METHOD_HW_DEVICE_CTX && - config->device_type == type) - { - hw_pix_fmt = config->pix_fmt; - break; - } - } - - if(av_hwdevice_ctx_create(&hw_device_ctx, type, NULL, NULL, 0) < 0) - throw VideoDecoderException("Failed to create hwdevice context"); - codec_context->hw_device_ctx = av_buffer_ref(hw_device_ctx); - } - - if(avcodec_open2(codec_context, codec, nullptr) < 0) - { - avcodec_free_context(&codec_context); - throw VideoDecoderException("Failed to open codec context"); - } -} - -VideoDecoder::~VideoDecoder() -{ - avcodec_close(codec_context); - avcodec_free_context(&codec_context); - if(hw_device_ctx) - { - av_buffer_unref(&hw_device_ctx); - } -} - -void VideoDecoder::PushFrame(uint8_t *buf, size_t buf_size) -{ - { - QMutexLocker locker(&mutex); - - AVPacket packet; - av_init_packet(&packet); - packet.data = buf; - packet.size = buf_size; - int r; -send_packet: - r = avcodec_send_packet(codec_context, &packet); - if(r != 0) - { - if(r == AVERROR(EAGAIN)) - { - CHIAKI_LOGE(log, "AVCodec internal buffer is full removing frames before pushing"); - AVFrame *frame = av_frame_alloc(); - if(!frame) - { - CHIAKI_LOGE(log, "Failed to alloc AVFrame"); - return; - } - r = avcodec_receive_frame(codec_context, frame); - av_frame_free(&frame); - if(r != 0) - { - CHIAKI_LOGE(log, "Failed to pull frame"); - return; - } - goto send_packet; - } - else - { - char errbuf[128]; - av_make_error_string(errbuf, sizeof(errbuf), r); - CHIAKI_LOGE(log, "Failed to push frame: %s", errbuf); - return; - } - } - } - - emit FramesAvailable(); -} - -AVFrame *VideoDecoder::PullFrame() -{ - QMutexLocker locker(&mutex); - - // always try to pull as much as possible and return only the very last frame - AVFrame *frame_last = nullptr; - AVFrame *sw_frame = nullptr; - AVFrame *frame = nullptr; - while(true) - { - AVFrame *next_frame; - if(frame_last) - { - av_frame_unref(frame_last); - next_frame = frame_last; - } - else - { - next_frame = av_frame_alloc(); - if(!next_frame) - return frame; - } - frame_last = frame; - frame = next_frame; - int r = avcodec_receive_frame(codec_context, frame); - if(r == 0) - { - frame = hw_decode_engine ? GetFromHardware(frame) : frame; - } - else - { - if(r != AVERROR(EAGAIN)) - CHIAKI_LOGE(log, "Decoding with FFMPEG failed"); - av_frame_free(&frame); - return frame_last; - } - } -} - -AVFrame *VideoDecoder::GetFromHardware(AVFrame *hw_frame) -{ - AVFrame *frame; - AVFrame *sw_frame; - - sw_frame = av_frame_alloc(); - - int ret = av_hwframe_transfer_data(sw_frame, hw_frame, 0); - - if(ret < 0) - { - CHIAKI_LOGE(log, "Failed to transfer frame from hardware"); - } - - av_frame_unref(hw_frame); - - if(sw_frame->width <= 0) - { - av_frame_unref(sw_frame); - return nullptr; - } - - return sw_frame; -} diff --git a/lib/CMakeLists.txt b/lib/CMakeLists.txt index f7fba38..e2a79d3 100644 --- a/lib/CMakeLists.txt +++ b/lib/CMakeLists.txt @@ -75,6 +75,12 @@ set(SOURCE_FILES src/regist.c src/opusdecoder.c) +if(CHIAKI_ENABLE_FFMPEG_DECODER) + list(APPEND HEADER_FILES include/chiaki/ffmpegdecoder.h) + list(APPEND SOURCE_FILES src/ffmpegdecoder.c) +endif() +set(CHIAKI_LIB_ENABLE_PI_DECODER "${CHIAKI_ENABLE_FFMPEG_DECODER}") + if(CHIAKI_ENABLE_PI_DECODER) list(APPEND HEADER_FILES include/chiaki/pidecoder.h) list(APPEND SOURCE_FILES src/pidecoder.c) @@ -127,6 +133,10 @@ endif() target_link_libraries(chiaki-lib Nanopb::nanopb) target_link_libraries(chiaki-lib Jerasure::Jerasure) +if(CHIAKI_ENABLE_FFMPEG_DECODER) + target_link_libraries(chiaki-lib FFMPEG::avcodec FFMPEG::avutil) +endif() + if(CHIAKI_ENABLE_PI_DECODER) target_link_libraries(chiaki-lib ILClient::ILClient) endif() diff --git a/lib/include/chiaki/common.h b/lib/include/chiaki/common.h index b035c9d..74bb344 100644 --- a/lib/include/chiaki/common.h +++ b/lib/include/chiaki/common.h @@ -100,6 +100,8 @@ static inline bool chiaki_codec_is_hdr(ChiakiCodec codec) return codec == CHIAKI_CODEC_H265_HDR; } +CHIAKI_EXPORT const char *chiaki_codec_name(ChiakiCodec codec); + #ifdef __cplusplus } #endif diff --git a/lib/include/chiaki/ffmpegdecoder.h b/lib/include/chiaki/ffmpegdecoder.h new file mode 100644 index 0000000..1389ca0 --- /dev/null +++ b/lib/include/chiaki/ffmpegdecoder.h @@ -0,0 +1,43 @@ +#ifndef CHIAKI_FFMPEG_DECODER_H +#define CHIAKI_FFMPEG_DECODER_H + +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +typedef struct chiaki_ffmpeg_decoder_t ChiakiFfmpegDecoder; + +typedef void (*ChiakiFfmpegFrameAvailable)(ChiakiFfmpegDecoder *decover, void *user); + +struct chiaki_ffmpeg_decoder_t +{ + ChiakiLog *log; + ChiakiMutex mutex; + AVCodec *av_codec; + AVCodecContext *codec_context; + enum AVPixelFormat hw_pix_fmt; + AVBufferRef *hw_device_ctx; + ChiakiMutex cb_mutex; + ChiakiFfmpegFrameAvailable frame_available_cb; + void *frame_available_cb_user; +}; + +CHIAKI_EXPORT ChiakiErrorCode chiaki_ffmpeg_decoder_init(ChiakiFfmpegDecoder *decoder, ChiakiLog *log, + ChiakiCodec codec, const char *hw_decoder_name, + ChiakiFfmpegFrameAvailable frame_available_cb, void *frame_available_cb_user); +CHIAKI_EXPORT void chiaki_ffmpeg_decoder_fini(ChiakiFfmpegDecoder *decoder); +CHIAKI_EXPORT bool chiaki_ffmpeg_decoder_video_sample_cb(uint8_t *buf, size_t buf_size, void *user); +CHIAKI_EXPORT AVFrame *chiaki_ffmpeg_decoder_pull_frame(ChiakiFfmpegDecoder *decoder); +CHIAKI_EXPORT enum AVPixelFormat chiaki_ffmpeg_decoder_get_pixel_format(ChiakiFfmpegDecoder *decoder); + +#ifdef __cplusplus +} +#endif + +#endif // CHIAKI_FFMPEG_DECODER_H diff --git a/lib/include/chiaki/log.h b/lib/include/chiaki/log.h index bfaeeba..75cddd9 100644 --- a/lib/include/chiaki/log.h +++ b/lib/include/chiaki/log.h @@ -50,6 +50,20 @@ CHIAKI_EXPORT void chiaki_log_hexdump_raw(ChiakiLog *log, ChiakiLogLevel level, #define CHIAKI_LOGW(log, ...) do { chiaki_log((log), CHIAKI_LOG_WARNING, __VA_ARGS__); } while(0) #define CHIAKI_LOGE(log, ...) do { chiaki_log((log), CHIAKI_LOG_ERROR, __VA_ARGS__); } while(0) +typedef struct chiaki_log_sniffer_t +{ + ChiakiLog *forward_log; // The original log, where everything is forwarded + ChiakiLog sniff_log; // The log where others will log into + uint32_t sniff_level_mask; + char *buf; // always null-terminated + size_t buf_len; // strlen(buf) +} ChiakiLogSniffer; + +CHIAKI_EXPORT void chiaki_log_sniffer_init(ChiakiLogSniffer *sniffer, uint32_t level_mask, ChiakiLog *forward_log); +CHIAKI_EXPORT void chiaki_log_sniffer_fini(ChiakiLogSniffer *sniffer); +static inline ChiakiLog *chiaki_log_sniffer_get_log(ChiakiLogSniffer *sniffer) { return &sniffer->sniff_log; } +static inline const char *chiaki_log_sniffer_get_buffer(ChiakiLogSniffer *sniffer) { return sniffer->buf; } + #ifdef __cplusplus } #endif diff --git a/lib/src/common.c b/lib/src/common.c index 21b8850..0dd2006 100644 --- a/lib/src/common.c +++ b/lib/src/common.c @@ -105,3 +105,18 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_lib_init() return CHIAKI_ERR_SUCCESS; } + +CHIAKI_EXPORT const char *chiaki_codec_name(ChiakiCodec codec) +{ + switch(codec) + { + case CHIAKI_CODEC_H264: + return "H264"; + case CHIAKI_CODEC_H265: + return "H265"; + case CHIAKI_CODEC_H265_HDR: + return "H265/HDR"; + default: + return "unknown"; + } +} diff --git a/lib/src/ffmpegdecoder.c b/lib/src/ffmpegdecoder.c new file mode 100644 index 0000000..5dd5bf0 --- /dev/null +++ b/lib/src/ffmpegdecoder.c @@ -0,0 +1,216 @@ + +#include + +#include + +static enum AVCodecID chiaki_codec_av_codec_id(ChiakiCodec codec) +{ + switch(codec) + { + case CHIAKI_CODEC_H265: + case CHIAKI_CODEC_H265_HDR: + return AV_CODEC_ID_H265; + default: + return AV_CODEC_ID_H264; + } +} + +CHIAKI_EXPORT ChiakiErrorCode chiaki_ffmpeg_decoder_init(ChiakiFfmpegDecoder *decoder, ChiakiLog *log, + ChiakiCodec codec, const char *hw_decoder_name, + ChiakiFfmpegFrameAvailable frame_available_cb, void *frame_available_cb_user) +{ + decoder->log = log; + decoder->frame_available_cb = frame_available_cb; + decoder->frame_available_cb_user = frame_available_cb_user; + + ChiakiErrorCode err = chiaki_mutex_init(&decoder->mutex, false); + if(err != CHIAKI_ERR_SUCCESS) + return err; + + decoder->hw_device_ctx = NULL; + decoder->hw_pix_fmt = AV_PIX_FMT_NONE; + +#if LIBAVCODEC_VERSION_INT < AV_VERSION_INT(58, 10, 100) + avcodec_register_all(); +#endif + enum AVCodecID av_codec = chiaki_codec_av_codec_id(codec); + decoder->av_codec = avcodec_find_decoder(av_codec); + if(!decoder->av_codec) + { + CHIAKI_LOGE(log, "%s Codec not available", chiaki_codec_name(codec)); + goto error_mutex; + } + + decoder->codec_context = avcodec_alloc_context3(decoder->av_codec); + if(!decoder->codec_context) + { + CHIAKI_LOGE(log, "Failed to alloc codec context"); + goto error_mutex; + } + + if(hw_decoder_name) + { + CHIAKI_LOGI(log, "Using hardware decoder \"%s\"", hw_decoder_name); + enum AVHWDeviceType type = av_hwdevice_find_type_by_name(hw_decoder_name); + if(type == AV_HWDEVICE_TYPE_NONE) + { + CHIAKI_LOGE(log, "Hardware decoder \"%s\" not found", hw_decoder_name); + goto error_codec_context; + } + + for(int i = 0;; i++) + { + const AVCodecHWConfig *config = avcodec_get_hw_config(decoder->av_codec, i); + if(!config) + { + CHIAKI_LOGE(log, "avcodec_get_hw_config failed"); + goto error_codec_context; + } + if(config->methods & AV_CODEC_HW_CONFIG_METHOD_HW_DEVICE_CTX && config->device_type == type) + { + decoder->hw_pix_fmt = config->pix_fmt; + break; + } + } + + if(av_hwdevice_ctx_create(&decoder->hw_device_ctx, type, NULL, NULL, 0) < 0) + { + CHIAKI_LOGE(log, "Failed to create hwdevice context"); + goto error_codec_context; + } + decoder->codec_context->hw_device_ctx = av_buffer_ref(decoder->hw_device_ctx); + } + + if(avcodec_open2(decoder->codec_context, decoder->av_codec, NULL) < 0) + { + CHIAKI_LOGE(log, "Failed to open codec context"); + goto error_codec_context; + } + + return CHIAKI_ERR_SUCCESS; +error_codec_context: + if(decoder->hw_device_ctx) + av_buffer_unref(&decoder->hw_device_ctx); + avcodec_free_context(&decoder->codec_context); +error_mutex: + chiaki_mutex_fini(&decoder->mutex); + return CHIAKI_ERR_UNKNOWN; +} + +CHIAKI_EXPORT void chiaki_ffmpeg_decoder_fini(ChiakiFfmpegDecoder *decoder) +{ + avcodec_close(decoder->codec_context); + avcodec_free_context(&decoder->codec_context); + if(decoder->hw_device_ctx) + av_buffer_unref(&decoder->hw_device_ctx); +} + +CHIAKI_EXPORT bool chiaki_ffmpeg_decoder_video_sample_cb(uint8_t *buf, size_t buf_size, void *user) +{ + ChiakiFfmpegDecoder *decoder = user; + + chiaki_mutex_lock(&decoder->mutex); + AVPacket packet; + av_init_packet(&packet); + packet.data = buf; + packet.size = buf_size; + int r; +send_packet: + r = avcodec_send_packet(decoder->codec_context, &packet); + if(r != 0) + { + if(r == AVERROR(EAGAIN)) + { + CHIAKI_LOGE(decoder->log, "AVCodec internal buffer is full removing frames before pushing"); + AVFrame *frame = av_frame_alloc(); + if(!frame) + { + CHIAKI_LOGE(decoder->log, "Failed to alloc AVFrame"); + goto hell; + } + r = avcodec_receive_frame(decoder->codec_context, frame); + av_frame_free(&frame); + if(r != 0) + { + CHIAKI_LOGE(decoder->log, "Failed to pull frame"); + goto hell; + } + goto send_packet; + } + else + { + char errbuf[128]; + av_make_error_string(errbuf, sizeof(errbuf), r); + CHIAKI_LOGE(decoder->log, "Failed to push frame: %s", errbuf); + goto hell; + } + } + chiaki_mutex_unlock(&decoder->mutex); + + decoder->frame_available_cb(decoder, decoder->frame_available_cb_user); + return true; +hell: + chiaki_mutex_unlock(&decoder->mutex); + return false; +} + +static AVFrame *pull_from_hw(ChiakiFfmpegDecoder *decoder, AVFrame *hw_frame) +{ + AVFrame *sw_frame = av_frame_alloc(); + if(av_hwframe_transfer_data(sw_frame, hw_frame, 0) < 0) + { + CHIAKI_LOGE(decoder->log, "Failed to transfer frame from hardware"); + av_frame_unref(sw_frame); + sw_frame = NULL; + } + av_frame_unref(hw_frame); + return sw_frame; +} + +CHIAKI_EXPORT AVFrame *chiaki_ffmpeg_decoder_pull_frame(ChiakiFfmpegDecoder *decoder) +{ + chiaki_mutex_lock(&decoder->mutex); + // always try to pull as much as possible and return only the very last frame + AVFrame *frame_last = NULL; + AVFrame *frame = NULL; + while(true) + { + AVFrame *next_frame; + if(frame_last) + { + av_frame_unref(frame_last); + next_frame = frame_last; + } + else + { + next_frame = av_frame_alloc(); + if(!next_frame) + break; + } + frame_last = frame; + frame = next_frame; + int r = avcodec_receive_frame(decoder->codec_context, frame); + if(!r) + frame = decoder->hw_device_ctx ? pull_from_hw(decoder, frame) : frame; + else + { + if(r != AVERROR(EAGAIN)) + CHIAKI_LOGE(decoder->log, "Decoding with FFMPEG failed"); + av_frame_free(&frame); + frame = frame_last; + break; + } + } + chiaki_mutex_unlock(&decoder->mutex); + + return frame; +} + +CHIAKI_EXPORT enum AVPixelFormat chiaki_ffmpeg_decoder_get_pixel_format(ChiakiFfmpegDecoder *decoder) +{ + // TODO: this is probably very wrong, especially for hdr + return decoder->hw_device_ctx + ? AV_PIX_FMT_NV12 + : AV_PIX_FMT_YUV420P; +} + diff --git a/lib/src/log.c b/lib/src/log.c index b62fc46..e0ba095 100644 --- a/lib/src/log.c +++ b/lib/src/log.c @@ -4,6 +4,7 @@ #include #include +#include CHIAKI_EXPORT char chiaki_log_level_char(ChiakiLogLevel level) { @@ -176,4 +177,51 @@ CHIAKI_EXPORT void chiaki_log_hexdump_raw(ChiakiLog *log, ChiakiLogLevel level, str[buf_size*2] = 0; chiaki_log(log, level, "%s", str); free(str); -} \ No newline at end of file +} + +static void log_sniffer_cb(ChiakiLogLevel level, const char *msg, void *user); + +CHIAKI_EXPORT void chiaki_log_sniffer_init(ChiakiLogSniffer *sniffer, uint32_t level_mask, ChiakiLog *forward_log) +{ + sniffer->forward_log = forward_log; + // level_mask is applied later and everything is forwarded unmasked, so use ALL here: + chiaki_log_init(&sniffer->sniff_log, CHIAKI_LOG_ALL, log_sniffer_cb, sniffer); + sniffer->sniff_level_mask = level_mask; + sniffer->buf = calloc(1, 1); + sniffer->buf_len = '\0'; +} + +CHIAKI_EXPORT void chiaki_log_sniffer_fini(ChiakiLogSniffer *sniffer) +{ + free(sniffer->buf); +} + +static void log_sniffer_push(ChiakiLogSniffer *sniffer, ChiakiLogLevel level, const char *msg) +{ + size_t len = strlen(msg); + if(!len) + return; + bool nl = sniffer->buf_len != 0; + char *new_buf = realloc(sniffer->buf, sniffer->buf_len + (nl ? 1 : 0) + 4 + len + 1); + if(!new_buf) + return; + sniffer->buf = new_buf; + if(nl) + sniffer->buf[sniffer->buf_len++] = '\n'; + sniffer->buf[sniffer->buf_len++] = '['; + sniffer->buf[sniffer->buf_len++] = chiaki_log_level_char(level); + sniffer->buf[sniffer->buf_len++] = ']'; + sniffer->buf[sniffer->buf_len++] = ' '; + memcpy(sniffer->buf + sniffer->buf_len, msg, len); + sniffer->buf_len += len; + sniffer->buf[sniffer->buf_len] = '\0'; +} + +static void log_sniffer_cb(ChiakiLogLevel level, const char *msg, void *user) +{ + ChiakiLogSniffer *sniffer = user; + if(level & sniffer->sniff_level_mask) + log_sniffer_push(sniffer, level, msg); + if(sniffer->forward_log) + chiaki_log(sniffer->forward_log, level, "%s", msg); +} From 7dd26f974f385f13fe9a863e72bae998ec68776f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Fri, 25 Dec 2020 21:39:58 +0100 Subject: [PATCH 105/237] Add new Console Icons to GUI --- assets/console.svg | 58 +++++++++-------- assets/console2.svg | 50 ++++++-------- gui/include/discoverymanager.h | 1 + gui/include/mainwindow.h | 2 + gui/include/servericonwidget.h | 7 +- gui/res/console-ps4.svg | 43 ++++++++++++ gui/res/console-ps5.svg | 42 ++++++++++++ gui/res/resources.qrc | 2 + gui/src/discoverymanager.cpp | 1 + gui/src/servericonwidget.cpp | 116 ++++++++++++++++++--------------- gui/src/serveritemwidget.cpp | 3 +- lib/include/chiaki/discovery.h | 1 + lib/src/discovery.c | 6 ++ 13 files changed, 222 insertions(+), 110 deletions(-) create mode 100644 gui/res/console-ps4.svg create mode 100644 gui/res/console-ps5.svg diff --git a/assets/console.svg b/assets/console.svg index 00fba66..99eadb9 100644 --- a/assets/console.svg +++ b/assets/console.svg @@ -1,6 +1,4 @@ - - @@ -25,18 +23,19 @@ borderopacity="1.0" inkscape:pageopacity="0.0" inkscape:pageshadow="2" - inkscape:zoom="1.4" - inkscape:cx="300.0445" - inkscape:cy="77.810757" + inkscape:zoom="1.979899" + inkscape:cx="277.12099" + inkscape:cy="63.078394" inkscape:document-units="mm" inkscape:current-layer="layer1" showgrid="false" units="px" - inkscape:window-width="1918" - inkscape:window-height="1048" + inkscape:window-width="1916" + inkscape:window-height="1031" inkscape:window-x="0" - inkscape:window-y="15" - inkscape:window-maximized="1" /> + inkscape:window-y="0" + inkscape:window-maximized="1" + inkscape:document-rotation="0" /> @@ -45,7 +44,7 @@ image/svg+xml - + @@ -54,21 +53,26 @@ inkscape:groupmode="layer" id="layer1" transform="translate(0,-274.24582)"> - - - - + + + diff --git a/assets/console2.svg b/assets/console2.svg index 39fb096..720fa37 100644 --- a/assets/console2.svg +++ b/assets/console2.svg @@ -23,11 +23,11 @@ borderopacity="1.0" inkscape:pageopacity="0.0" inkscape:pageshadow="2" - inkscape:zoom="0.7" - inkscape:cx="87.618933" - inkscape:cy="14.057308" + inkscape:zoom="0.98994949" + inkscape:cx="162.87247" + inkscape:cy="106.01511" inkscape:document-units="mm" - inkscape:current-layer="g1675" + inkscape:current-layer="layer1" showgrid="false" units="px" inkscape:window-width="1916" @@ -50,32 +50,22 @@ - - - - - - + inkscape:groupmode="layer"> + + + diff --git a/gui/include/discoverymanager.h b/gui/include/discoverymanager.h index f03a5e6..be3462c 100644 --- a/gui/include/discoverymanager.h +++ b/gui/include/discoverymanager.h @@ -12,6 +12,7 @@ struct DiscoveryHost { + bool ps5; ChiakiDiscoveryHostState state; uint16_t host_request_port; #define STRING_MEMBER(name) QString name; diff --git a/gui/include/mainwindow.h b/gui/include/mainwindow.h index 715c7d9..f1190de 100644 --- a/gui/include/mainwindow.h +++ b/gui/include/mainwindow.h @@ -22,6 +22,8 @@ struct DisplayServer bool registered; QString GetHostAddr() const { return discovered ? discovery_host.host_addr : manual_host.GetHost(); } + bool IsPS5() const { return discovered ? discovery_host.ps5 : + (registered ? chiaki_target_is_ps5(registered_host.GetTarget()) : false); } }; class MainWindow : public QMainWindow diff --git a/gui/include/servericonwidget.h b/gui/include/servericonwidget.h index 1a8525a..364cb4b 100644 --- a/gui/include/servericonwidget.h +++ b/gui/include/servericonwidget.h @@ -6,13 +6,18 @@ #include #include +#include class ServerIconWidget : public QWidget { Q_OBJECT private: + bool ps5 = false; ChiakiDiscoveryHostState state = CHIAKI_DISCOVERY_HOST_STATE_UNKNOWN; + QSvgRenderer svg_renderer; + + void LoadSvg(); protected: void paintEvent(QPaintEvent *event) override; @@ -20,7 +25,7 @@ class ServerIconWidget : public QWidget public: explicit ServerIconWidget(QWidget *parent = nullptr); - void SetState(ChiakiDiscoveryHostState state) { this->state = state; update(); } + void SetState(bool ps5, ChiakiDiscoveryHostState state); }; #endif // CHIAKI_SERVERICONWIDGET_H diff --git a/gui/res/console-ps4.svg b/gui/res/console-ps4.svg new file mode 100644 index 0000000..1591574 --- /dev/null +++ b/gui/res/console-ps4.svg @@ -0,0 +1,43 @@ + + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/gui/res/console-ps5.svg b/gui/res/console-ps5.svg new file mode 100644 index 0000000..8216a04 --- /dev/null +++ b/gui/res/console-ps5.svg @@ -0,0 +1,42 @@ + + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/gui/res/resources.qrc b/gui/res/resources.qrc index bf7efd5..86efda9 100644 --- a/gui/res/resources.qrc +++ b/gui/res/resources.qrc @@ -5,5 +5,7 @@ discover-24px.svg discover-off-24px.svg chiaki.svg + console-ps4.svg + console-ps5.svg diff --git a/gui/src/discoverymanager.cpp b/gui/src/discoverymanager.cpp index 042e68b..ae1564f 100644 --- a/gui/src/discoverymanager.cpp +++ b/gui/src/discoverymanager.cpp @@ -129,6 +129,7 @@ static void DiscoveryServiceHostsCallback(ChiakiDiscoveryHost *hosts, size_t hos { ChiakiDiscoveryHost *h = hosts + i; DiscoveryHost o = {}; + o.ps5 = chiaki_discovery_host_is_ps5(h); o.state = h->state; o.host_request_port = o.host_request_port; #define CONVERT_STRING(name) if(h->name) { o.name = QString::fromLocal8Bit(h->name); } diff --git a/gui/src/servericonwidget.cpp b/gui/src/servericonwidget.cpp index d001026..45d85a5 100644 --- a/gui/src/servericonwidget.cpp +++ b/gui/src/servericonwidget.cpp @@ -7,73 +7,87 @@ ServerIconWidget::ServerIconWidget(QWidget *parent) : QWidget(parent) { + LoadSvg(); } void ServerIconWidget::paintEvent(QPaintEvent *event) { - static const float icon_aspect = 100.0f / 17.0f; - static const float coolness = 0.585913713f; - - if(width() == 0 || height() == 0) + QRectF view_box = svg_renderer.viewBoxF(); + if(view_box.width() < 0.00001f || view_box.height() < 0.00001f) return; + float icon_aspect = view_box.width() / view_box.height(); + float widget_aspect = (float)width() / (float)height(); + QRectF icon_rect; + if(widget_aspect > icon_aspect) + { + icon_rect.setHeight(height()); + icon_rect.setWidth((float)height() * icon_aspect); + icon_rect.moveTop(0.0f); + icon_rect.moveLeft(((float)width() - icon_rect.width()) * 0.5f); + } + else + { + icon_rect.setWidth(width()); + icon_rect.setHeight((float)width() / icon_aspect); + icon_rect.moveLeft(0.0f); + icon_rect.moveTop(((float)height() - icon_rect.height()) * 0.5f); + } QPainter painter(this); painter.setRenderHint(QPainter::Antialiasing); - QRectF icon_rect; - float widget_aspect = (float)width() / (float)height(); - if(widget_aspect > icon_aspect) - { - icon_rect.setHeight(height()); - icon_rect.setWidth((float)height() * icon_aspect); - icon_rect.moveTop(0.0f); - icon_rect.moveLeft(((float)width() - icon_rect.width()) * 0.5f); - } - else - { - icon_rect.setWidth(width()); - icon_rect.setHeight((float)width() / icon_aspect); - icon_rect.moveLeft(0.0f); - icon_rect.moveTop(((float)height() - icon_rect.height()) * 0.5f); - } - - - auto XForY = [&icon_rect](float y, bool right) - { - float r = (icon_rect.height() - y) * coolness; - if(right) - r += icon_rect.width() - icon_rect.height() * coolness; - return r; + auto render_element = [&view_box, &icon_rect, this](QPainter &painter, const QString &id) { + QRectF src = svg_renderer.transformForElement(id).mapRect(svg_renderer.boundsOnElement(id)); + QRectF dst = src.translated(-view_box.left(), -view_box.top()); + dst = QRectF( + icon_rect.width() * dst.left() / view_box.width(), + icon_rect.height() * dst.top() / view_box.height(), + icon_rect.width() * dst.width() / view_box.width(), + icon_rect.height() * dst.height() / view_box.height()); + dst = dst.translated(icon_rect.left(), icon_rect.top()); + svg_renderer.render(&painter, id, dst); }; - auto SectionPath = [&XForY, &icon_rect](float y0, float y1) - { - QPainterPath path; - path.moveTo(XForY(y0, false), y0); - path.lineTo(XForY(y1, false), y1); - path.lineTo(XForY(y1, true), y1); - path.lineTo(XForY(y0, true), y0); - path.translate(icon_rect.topLeft()); - return path; - }; - - auto color = palette().color(QPalette::Text); - - QColor bar_color; switch(state) { - case CHIAKI_DISCOVERY_HOST_STATE_STANDBY: - bar_color = QColor(255, 174, 47); - break; case CHIAKI_DISCOVERY_HOST_STATE_READY: - bar_color = QColor(0, 167, 255); + render_element(painter, "light_on"); + break; + case CHIAKI_DISCOVERY_HOST_STATE_STANDBY: + render_element(painter, "light_standby"); break; default: break; } - if(bar_color.isValid()) - painter.fillPath(SectionPath(icon_rect.height() * 0.41f - 1.0f, icon_rect.height() * 0.59f + 1.0f), bar_color); - painter.fillPath(SectionPath(0, icon_rect.height() * 0.41f), color); - painter.fillPath(SectionPath(icon_rect.height() * 0.59f, icon_rect.height()), color); -} \ No newline at end of file + auto color = palette().color(QPalette::Text); + + QImage temp_image((int)(devicePixelRatioF() * width()), + (int)(devicePixelRatioF() * height()), + QImage::Format_ARGB32_Premultiplied); + { + temp_image.fill(QColor(0, 0, 0, 0)); + QPainter temp_painter(&temp_image); + render_element(temp_painter, "console"); + temp_painter.setCompositionMode(QPainter::CompositionMode_SourceAtop); + temp_painter.fillRect(icon_rect, color); + } + painter.drawImage(QRectF(0.0f, 0.0f, width(), height()), temp_image); +} + +void ServerIconWidget::LoadSvg() +{ + QString path = ps5 ? ":/icons/console-ps5.svg" : ":/icons/console-ps4.svg"; + svg_renderer.load(path); + update(); +} + +void ServerIconWidget::SetState(bool ps5, ChiakiDiscoveryHostState state) +{ + bool reload = this->ps5 != ps5; + this->ps5 = ps5; + this->state = state; + if(reload) + LoadSvg(); + update(); +} diff --git a/gui/src/serveritemwidget.cpp b/gui/src/serveritemwidget.cpp index ef7448e..13e1937 100644 --- a/gui/src/serveritemwidget.cpp +++ b/gui/src/serveritemwidget.cpp @@ -70,7 +70,8 @@ void ServerItemWidget::Update(const DisplayServer &display_server) delete_action->setEnabled(!display_server.discovered); wake_action->setEnabled(display_server.registered); - icon_widget->SetState(display_server.discovered ? display_server.discovery_host.state : CHIAKI_DISCOVERY_HOST_STATE_UNKNOWN); + icon_widget->SetState(display_server.IsPS5(), + display_server.discovered ? display_server.discovery_host.state : CHIAKI_DISCOVERY_HOST_STATE_UNKNOWN); QString top_text = ""; diff --git a/lib/include/chiaki/discovery.h b/lib/include/chiaki/discovery.h index 50364ac..ddae8ef 100644 --- a/lib/include/chiaki/discovery.h +++ b/lib/include/chiaki/discovery.h @@ -71,6 +71,7 @@ typedef struct chiaki_discovery_host_t #undef STRING_MEMBER } ChiakiDiscoveryHost; +CHIAKI_EXPORT bool chiaki_discovery_host_is_ps5(ChiakiDiscoveryHost *host); CHIAKI_EXPORT int chiaki_discovery_packet_fmt(char *buf, size_t buf_size, ChiakiDiscoveryPacket *packet); diff --git a/lib/src/discovery.c b/lib/src/discovery.c index 4771456..601e488 100644 --- a/lib/src/discovery.c +++ b/lib/src/discovery.c @@ -32,6 +32,12 @@ const char *chiaki_discovery_host_state_string(ChiakiDiscoveryHostState state) } } +CHIAKI_EXPORT bool chiaki_discovery_host_is_ps5(ChiakiDiscoveryHost *host) +{ + return host->device_discovery_protocol_version + && !strcmp(host->device_discovery_protocol_version, CHIAKI_DISCOVERY_PROTOCOL_VERSION_PS5); +} + CHIAKI_EXPORT int chiaki_discovery_packet_fmt(char *buf, size_t buf_size, ChiakiDiscoveryPacket *packet) { if(!packet->protocol_version) From 1dfe88d74e74dd12488a32218c8b3b87866cdbad Mon Sep 17 00:00:00 2001 From: H0neyBadger Date: Sat, 26 Dec 2020 10:32:47 +0100 Subject: [PATCH 106/237] Add discovery system_version to ChiakiTarget translator (#1) --- lib/include/chiaki/discovery.h | 2 ++ lib/src/discovery.c | 26 ++++++++++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/lib/include/chiaki/discovery.h b/lib/include/chiaki/discovery.h index ddae8ef..c9881b5 100644 --- a/lib/include/chiaki/discovery.h +++ b/lib/include/chiaki/discovery.h @@ -73,6 +73,8 @@ typedef struct chiaki_discovery_host_t CHIAKI_EXPORT bool chiaki_discovery_host_is_ps5(ChiakiDiscoveryHost *host); +CHIAKI_EXPORT ChiakiTarget chiaki_discovery_host_system_version_target(ChiakiDiscoveryHost *host); + CHIAKI_EXPORT int chiaki_discovery_packet_fmt(char *buf, size_t buf_size, ChiakiDiscoveryPacket *packet); typedef struct chiaki_discovery_t diff --git a/lib/src/discovery.c b/lib/src/discovery.c index 601e488..0658c87 100644 --- a/lib/src/discovery.c +++ b/lib/src/discovery.c @@ -38,6 +38,32 @@ CHIAKI_EXPORT bool chiaki_discovery_host_is_ps5(ChiakiDiscoveryHost *host) && !strcmp(host->device_discovery_protocol_version, CHIAKI_DISCOVERY_PROTOCOL_VERSION_PS5); } +CHIAKI_EXPORT ChiakiTarget chiaki_discovery_host_system_version_target(ChiakiDiscoveryHost *host) +{ + // traslate discovered system_version into ChiakiTarget + + int version = atoi(host->system_version); + bool is_ps5 = chiaki_discovery_host_is_ps5(host); + + if(version >= 8050001 && is_ps5) + // PS5 >= 1.0 + return CHIAKI_TARGET_PS5_1; + if(version >= 8050000 && is_ps5) + // PS5 >= 0 + return CHIAKI_TARGET_PS5_UNKNOWN; + + if(version >= 8000000) + // PS4 >= 8.0 + return CHIAKI_TARGET_PS4_10; + if(version >= 7000000) + // PS4 >= 7.0 + return CHIAKI_TARGET_PS4_9; + if(version > 0) + return CHIAKI_TARGET_PS4_8; + + return CHIAKI_TARGET_PS4_UNKNOWN; +} + CHIAKI_EXPORT int chiaki_discovery_packet_fmt(char *buf, size_t buf_size, ChiakiDiscoveryPacket *packet) { if(!packet->protocol_version) From 61823f73724bca71fde19b7fa0589141f74b8dd7 Mon Sep 17 00:00:00 2001 From: h0neybadger Date: Thu, 24 Dec 2020 12:06:44 +0100 Subject: [PATCH 107/237] Remove PS4 references and use server or PlayStation names instead --- switch/README.md | 16 ++++++++-------- switch/include/gui.h | 6 +++--- switch/include/host.h | 4 ++-- switch/src/gui.cpp | 26 +++++++++++++------------- switch/src/host.cpp | 4 ++-- 5 files changed, 28 insertions(+), 28 deletions(-) diff --git a/switch/README.md b/switch/README.md index 9a5f6db..709f900 100644 --- a/switch/README.md +++ b/switch/README.md @@ -47,16 +47,16 @@ Chiaki config file The **chiaki.conf** is generated by the application. this file contains sensitive data. (do not share this file) ```ini -# required: PS4-XXX (PS4 local name) -# name from PS4 Settings > System > system information -[PS4-XXX] -# required: lan PS4 IP address -# IP from PS4 Settings > System > system information -host_ip = X.X.X.X +# required: PS*-*** (PS4/PS5 local name) +# name from Settings > System > system information +[PS*-***] +# required: lan PlayStation IP address +# IP from Settings > System > system information +host_ip = *.*.*.* # required: sony oline id (login) -psn_online_id = ps4_online_id +psn_online_id = ps_online_id # required (PS4>7.0 Only): https://git.sr.ht/~thestr4ng3r/chiaki/tree/master/item/README.md#obtaining-your-psn-accountid -psn_account_id = ps4_base64_account_id +psn_account_id = ps_base64_account_id # optional(default 60): remote play fps (must be 30 or 60) video_fps = 60 # optional(default 720p): remote play resolution (must be 720p, 540p or 360p) diff --git a/switch/include/gui.h b/switch/include/gui.h index 2f07e73..0d41ee4 100644 --- a/switch/include/gui.h +++ b/switch/include/gui.h @@ -61,7 +61,7 @@ class MainApplication bool Load(); }; -class PS4RemotePlay : public brls::View +class PSRemotePlay : public brls::View { private: brls::AppletFrame * frame; @@ -77,8 +77,8 @@ class PS4RemotePlay : public brls::View // int fps = 0; public: - PS4RemotePlay(IO * io, Host * host); - ~PS4RemotePlay(); + PSRemotePlay(IO * io, Host * host); + ~PSRemotePlay(); void draw(NVGcontext * vg, int x, int y, unsigned width, unsigned height, brls::Style * style, brls::FrameContext * ctx) override; }; diff --git a/switch/include/host.h b/switch/include/host.h index 6be0b14..26f3c8b 100644 --- a/switch/include/host.h +++ b/switch/include/host.h @@ -59,9 +59,9 @@ class Host std::string ap_bssid; std::string ap_key; std::string ap_name; - std::string ps4_nickname; + std::string server_nickname; // mac address = 48 bits - uint8_t ps4_mac[6] = {0}; + uint8_t server_mac[6] = {0}; char rp_regist_key[CHIAKI_SESSION_AUTH_SIZE] = {0}; uint32_t rp_key_type = 0; uint8_t rp_key[0x10] = {0}; diff --git a/switch/src/gui.cpp b/switch/src/gui.cpp index 1f29410..77a71e6 100644 --- a/switch/src/gui.cpp +++ b/switch/src/gui.cpp @@ -52,12 +52,12 @@ void HostInterface::Register(bool pin_incorrect) { if(pin_incorrect) { - DIALOG(srfpvyps, "Session Registration Failed, Please verify your PS4 settings"); + DIALOG(srfpvyps, "Session Registration Failed, Please verify your PlayStation settings"); return; } // the host is not registered yet - brls::Dialog* peprpc = new brls::Dialog("Please enter your PS4 registration PIN code"); + brls::Dialog* peprpc = new brls::Dialog("Please enter your PlayStation registration PIN code"); brls::GenericEvent::Callback cb_peprpc = [this, peprpc](brls::View* view) { bool pin_provided = false; @@ -101,18 +101,18 @@ void HostInterface::Wakeup(brls::View * view) if(!this->host->rp_key_data) { // the host is not registered yet - DIALOG(prypf, "Please register your PS4 first"); + DIALOG(prypf, "Please register your PlayStation first"); } else { int r = host->Wakeup(); if(r == 0) { - brls::Application::notify("PS4 Wakeup packet sent"); + brls::Application::notify("PlayStation Wakeup packet sent"); } else { - brls::Application::notify("PS4 Wakeup packet failed"); + brls::Application::notify("PlayStation Wakeup packet failed"); } } } @@ -123,7 +123,7 @@ void HostInterface::Connect(brls::View * view) if(this->host->state != CHIAKI_DISCOVERY_HOST_STATE_READY) { // host in standby mode - DIALOG(ptoyp, "Please turn on your PS4"); + DIALOG(ptoyp, "Please turn on your PlayStation"); return; } @@ -211,7 +211,7 @@ bool HostInterface::Stream() // brls::Application::setDisplayFramerate(true); // push raw opengl stream over borealis - brls::Application::pushView(new PS4RemotePlay(this->io, this->host)); + brls::Application::pushView(new PSRemotePlay(this->io, this->host)); return true; } @@ -276,7 +276,7 @@ bool MainApplication::Load() // Create a view this->rootFrame = new brls::TabFrame(); - this->rootFrame->setTitle("Chiaki: Open Source PS4 Remote Play Client"); + this->rootFrame->setTitle("Chiaki: Open Source PlayStation Remote Play Client"); this->rootFrame->setIcon(BOREALIS_ASSET("icon.jpg")); brls::List* config = new brls::List(); @@ -428,12 +428,12 @@ bool MainApplication::BuildConfigurationMenu(brls::List * ls, Host * host) ls->addView(info); std::string host_name_string = this->settings->GetHostName(host); - brls::ListItem* host_name = new brls::ListItem("PS4 Hostname"); + brls::ListItem* host_name = new brls::ListItem("PS Hostname"); host_name->setValue(host_name_string.c_str()); ls->addView(host_name); std::string host_ipaddr_string = settings->GetHostIPAddr(host); - brls::ListItem* host_ipaddr = new brls::ListItem("PS4 IP Address"); + brls::ListItem* host_ipaddr = new brls::ListItem("PS IP Address"); host_ipaddr->setValue(host_ipaddr_string.c_str()); ls->addView(host_ipaddr); @@ -457,7 +457,7 @@ bool MainApplication::BuildConfigurationMenu(brls::List * ls, Host * host) return true; } -PS4RemotePlay::PS4RemotePlay(IO * io, Host * host) +PSRemotePlay::PSRemotePlay(IO * io, Host * host) : io(io), host(host) { // store joycon/touchpad keys @@ -468,7 +468,7 @@ PS4RemotePlay::PS4RemotePlay(IO * io, Host * host) // this->base_time=glfwGetTime(); } -void PS4RemotePlay::draw(NVGcontext* vg, int x, int y, unsigned width, unsigned height, brls::Style* style, brls::FrameContext* ctx) +void PSRemotePlay::draw(NVGcontext* vg, int x, int y, unsigned width, unsigned height, brls::Style* style, brls::FrameContext* ctx) { this->io->MainLoop(&this->state); this->host->SendFeedbackState(&state); @@ -493,7 +493,7 @@ void PS4RemotePlay::draw(NVGcontext* vg, int x, int y, unsigned width, unsigned // nvgText(vg, 5,10, fps_str, NULL); } -PS4RemotePlay::~PS4RemotePlay() +PSRemotePlay::~PSRemotePlay() { } diff --git a/switch/src/host.cpp b/switch/src/host.cpp index 3779377..797c088 100644 --- a/switch/src/host.cpp +++ b/switch/src/host.cpp @@ -230,8 +230,8 @@ void Host::RegistCB(ChiakiRegistEvent * event) this->ap_ssid = r_host->ap_ssid; this->ap_key = r_host->ap_key; this->ap_name = r_host->ap_name; - memcpy( &(this->ps4_mac), &(r_host->ps4_mac), sizeof(this->ps4_mac) ); - this->ps4_nickname = r_host->ps4_nickname; + memcpy( &(this->server_mac), &(r_host->server_mac), sizeof(this->server_mac) ); + this->server_nickname = r_host->server_nickname; memcpy( &(this->rp_regist_key), &(r_host->rp_regist_key), sizeof(this->rp_regist_key) ); this->rp_key_type = r_host->rp_key_type; memcpy( &(this->rp_key), &(r_host->rp_key), sizeof(this->rp_key) ); From d5945f14207af8141d452bc175f3f6c7359e132b Mon Sep 17 00:00:00 2001 From: h0neybadger Date: Wed, 23 Dec 2020 10:15:37 +0100 Subject: [PATCH 108/237] Add switch #402 "add host" menu for remote connection currently working on the "remove host" feature https://github.com/natinusala/borealis/issues/75 --- switch/include/gui.h | 10 +- switch/include/settings.h | 6 +- switch/src/gui.cpp | 222 +++++++++++++++++++++++++++++--------- switch/src/settings.cpp | 19 ++-- 4 files changed, 190 insertions(+), 67 deletions(-) diff --git a/switch/include/gui.h b/switch/include/gui.h index 0d41ee4..9ebbab4 100644 --- a/switch/include/gui.h +++ b/switch/include/gui.h @@ -32,7 +32,9 @@ class HostInterface HostInterface(brls::List * hostList, IO * io, Host * host, Settings * settings); ~HostInterface(); - void Register(bool pin_incorrect); + static void Register(IO * io, Host * host, + Settings * settings, std::function success_cb = nullptr); + void Register(); void Wakeup(brls::View * view); void Connect(brls::View * view); void ConnectSession(); @@ -51,7 +53,13 @@ class MainApplication IO * io; brls::TabFrame * rootFrame; std::map host_menuitems; + // add_host local settings + std::string remote_display_name = ""; + std::string remote_addr = ""; + int remote_ps4_version = 8000000; + bool BuildConfigurationMenu(brls::List *, Host * host = nullptr); + void BuildAddHostConfigurationMenu(brls::List *); public: MainApplication(std::map * hosts, Settings * settings, DiscoveryManager * discoverymanager, diff --git a/switch/include/settings.h b/switch/include/settings.h index 1a8eb03..176f864 100644 --- a/switch/include/settings.h +++ b/switch/include/settings.h @@ -28,7 +28,7 @@ class Settings { UNKNOWN, HOST_NAME, - HOST_IP, + HOST_ADDR, PSN_ONLINE_ID, PSN_ACCOUNT_ID, RP_KEY, @@ -43,7 +43,7 @@ class Settings // the goal is to read/write inernal flat configuration file const std::map re_map = { { HOST_NAME, std::regex("^\\[\\s*(.+)\\s*\\]") }, - { HOST_IP, std::regex("^\\s*host_ip\\s*=\\s*\"?(\\d+\\.\\d+\\.\\d+\\.\\d+)\"?") }, + { HOST_ADDR, std::regex("^\\s*host_(?:ip|addr)\\s*=\\s*\"?((\\d+\\.\\d+\\.\\d+\\.\\d+)|([A-Za-z0-9-]{1,255}))\"?") }, { PSN_ONLINE_ID, std::regex("^\\s*psn_online_id\\s*=\\s*\"?(\\w+)\"?") }, { PSN_ACCOUNT_ID, std::regex("^\\s*psn_account_id\\s*=\\s*\"?([\\w/=+]+)\"?") }, { RP_KEY, std::regex("^\\s*rp_key\\s*=\\s*\"?([\\w/=+]+)\"?") }, @@ -75,7 +75,7 @@ class Settings void SetVideoFPS(Host * host, ChiakiVideoFPSPreset value); void SetVideoResolution(Host * host, std::string value); void SetVideoFPS(Host * host, std::string value); - std::string GetHostIPAddr(Host * host); + std::string GetHostAddr(Host * host); std::string GetHostName(Host * host); bool SetHostRPKeyType(Host * host, std::string value); int GetHostRPKeyType(Host * host); diff --git a/switch/src/gui.cpp b/switch/src/gui.cpp index 77a71e6..4ffecfc 100644 --- a/switch/src/gui.cpp +++ b/switch/src/gui.cpp @@ -48,17 +48,53 @@ HostInterface::~HostInterface() Disconnect(); } -void HostInterface::Register(bool pin_incorrect) +void HostInterface::Register(IO * io, Host * host, Settings * settings, std::function success_cb) { - if(pin_incorrect) + // user must provide psn id for registration + std::string account_id = settings->GetPSNAccountID(host); + std::string online_id = settings->GetPSNOnlineID(host); + if(host->system_version >= 7000000 && account_id.length() <= 0) { - DIALOG(srfpvyps, "Session Registration Failed, Please verify your PlayStation settings"); + // PS4 firmware > 7.0 + DIALOG(upaid, "Undefined PSN Account ID (Please configure a valid psn_account_id)"); + return; + } + else if( host->system_version < 7000000 && host->system_version > 0 && online_id.length() <= 0) + { + // use oline ID for ps4 < 7.0 + DIALOG(upoid, "Undefined PSN Online ID (Please configure a valid psn_online_id)"); return; } + // add HostConnected function to regist_event_type_finished_success + auto event_type_finished_success_cb = [settings, success_cb]() + { + // save RP keys + settings->WriteFile(); + if(success_cb != nullptr) + { + // FIXME: may raise a connection refused + // when the connection is triggered + // just after the register success + sleep(2); + success_cb(); + } + // decrement block input token number + brls::Application::unblockInputs(); + }; + host->SetRegistEventTypeFinishedSuccess(event_type_finished_success_cb); + + auto event_type_finished_failed_cb = []() + { + // unlock user inputs + brls::Application::unblockInputs(); + brls::Application::notify("Registration failed"); + }; + host->SetRegistEventTypeFinishedFailed(event_type_finished_failed_cb); + // the host is not registered yet brls::Dialog* peprpc = new brls::Dialog("Please enter your PlayStation registration PIN code"); - brls::GenericEvent::Callback cb_peprpc = [this, peprpc](brls::View* view) + brls::GenericEvent::Callback cb_peprpc = [host, io, peprpc](brls::View* view) { bool pin_provided = false; char pin_input[9] = {0}; @@ -68,12 +104,12 @@ void HostInterface::Register(bool pin_incorrect) // before the the ReadUserKeyboard peprpc->close(); - pin_provided = this->io->ReadUserKeyboard(pin_input, sizeof(pin_input)); + pin_provided = io->ReadUserKeyboard(pin_input, sizeof(pin_input)); if(pin_provided) { // prevent users form messing with the gui brls::Application::blockInputs(); - int ret = this->host->Register(pin_input); + int ret = host->Register(pin_input); if(ret != HOST_REGISTER_OK) { switch(ret) @@ -96,6 +132,13 @@ void HostInterface::Register(bool pin_incorrect) peprpc->open(); } +void HostInterface::Register() +{ + // use Connect just after the registration to save user inputs + HostInterface::Register(this->io, this->host, + this->settings, std::bind(&HostInterface::ConnectSession, this)); +} + void HostInterface::Wakeup(brls::View * view) { if(!this->host->rp_key_data) @@ -120,7 +163,15 @@ void HostInterface::Wakeup(brls::View * view) void HostInterface::Connect(brls::View * view) { // check that all requirements are met - if(this->host->state != CHIAKI_DISCOVERY_HOST_STATE_READY) + if(!this->host->discovered && !this->host->rp_key_data) + { + // at this point the host must be discovered or registered manually + // to validate the system_version accuracy + brls::Application::crash("Undefined PlayStation remote version"); + } + + // ignore state for remote hosts + if(this->host->discovered && this->host->state != CHIAKI_DISCOVERY_HOST_STATE_READY) { // host in standby mode DIALOG(ptoyp, "Please turn on your PlayStation"); @@ -129,46 +180,7 @@ void HostInterface::Connect(brls::View * view) if(!this->host->rp_key_data) { - // user must provide psn id for registration - std::string account_id = this->settings->GetPSNAccountID(this->host); - std::string online_id = this->settings->GetPSNOnlineID(this->host); - if(this->host->system_version >= 7000000 && account_id.length() <= 0) - { - // PS4 firmware > 7.0 - DIALOG(upaid, "Undefined PSN Account ID (Please configure a valid psn_account_id)"); - return; - } - else if( this->host->system_version < 7000000 && this->host->system_version > 0 && online_id.length() <= 0) - { - // use oline ID for ps4 < 7.0 - DIALOG(upoid, "Undefined PSN Online ID (Please configure a valid psn_online_id)"); - return; - } - - // add HostConnected function to regist_event_type_finished_success - auto event_type_finished_success_cb = [this]() - { - // save RP keys - this->settings->WriteFile(); - // FIXME: may raise a connection refused - // when the connection is triggered - // just after the register success - sleep(2); - ConnectSession(); - // decrement block input token number - brls::Application::unblockInputs(); - }; - this->host->SetRegistEventTypeFinishedSuccess(event_type_finished_success_cb); - - auto event_type_finished_failed_cb = [this]() - { - // unlock user inputs - brls::Application::unblockInputs(); - brls::Application::notify("Registration failed"); - }; - this->host->SetRegistEventTypeFinishedFailed(event_type_finished_failed_cb); - - this->Register(false); + this->Register(); } else { @@ -280,9 +292,12 @@ bool MainApplication::Load() this->rootFrame->setIcon(BOREALIS_ASSET("icon.jpg")); brls::List* config = new brls::List(); - BuildConfigurationMenu(config); + brls::List* add_host = new brls::List(); + BuildConfigurationMenu(config); + BuildAddHostConfigurationMenu(add_host); this->rootFrame->addTab("Configuration", config); + this->rootFrame->addTab("Add Host", add_host); // ---------------- this->rootFrame->addSeparator(); @@ -292,7 +307,9 @@ bool MainApplication::Load() { for(auto it = this->hosts->begin(); it != this->hosts->end(); it++) { - if(this->host_menuitems.find(&it->second) == this->host_menuitems.end()) + // add host to the gui only if the host is registered or discovered + if(this->host_menuitems.find(&it->second) == this->host_menuitems.end() + && (it->second.rp_key_data == true || it->second.discovered == true)) { brls::List* new_host = new brls::List(); this->host_menuitems[&it->second] = new_host; @@ -432,10 +449,10 @@ bool MainApplication::BuildConfigurationMenu(brls::List * ls, Host * host) host_name->setValue(host_name_string.c_str()); ls->addView(host_name); - std::string host_ipaddr_string = settings->GetHostIPAddr(host); - brls::ListItem* host_ipaddr = new brls::ListItem("PS IP Address"); - host_ipaddr->setValue(host_ipaddr_string.c_str()); - ls->addView(host_ipaddr); + std::string host_addr_string = settings->GetHostAddr(host); + brls::ListItem* host_addr = new brls::ListItem("PS4 Address"); + host_addr->setValue(host_addr_string.c_str()); + ls->addView(host_addr); std::string host_rp_regist_key_string = settings->GetHostRPRegistKey(host); brls::ListItem* host_rp_regist_key = new brls::ListItem("RP Register Key"); @@ -457,6 +474,105 @@ bool MainApplication::BuildConfigurationMenu(brls::List * ls, Host * host) return true; } +void MainApplication::BuildAddHostConfigurationMenu(brls::List * add_host) +{ + // create host for wan connection + // brls::Label* add_host_label = new brls::Label(brls::LabelStyle::REGULAR, + // "Add Host configuration", true); + + brls::ListItem* display_name = new brls::ListItem("Display name"); + auto display_name_cb = [this, display_name](brls::View * view) + { + char name[16] = {0}; + bool input = this->io->ReadUserKeyboard(name, sizeof(name)); + if(input) + { + // update gui + display_name->setValue(name); + // set internal value + this->remote_display_name = name; + } + }; + display_name->getClickEvent()->subscribe(display_name_cb); + add_host->addView(display_name); + + brls::ListItem* address = new brls::ListItem("Remote IP/name"); + auto address_cb = [this, address](brls::View * view) + { + char addr[256] = {0}; + bool input = this->io->ReadUserKeyboard(addr, sizeof(addr)); + if(input) + { + // update gui + address->setValue(addr); + // set internal value + this->remote_addr = addr; + } + }; + address->getClickEvent()->subscribe(address_cb); + add_host->addView(address); + + + // TODO + // brls::ListItem* port = new brls::ListItem("Remote session port", "tcp/udp 9295"); + // brls::ListItem* port = new brls::ListItem("Remote stream port", "udp 9296"); + // brls::ListItem* port = new brls::ListItem("Remote Senkusha port", "udp 9297"); + brls::SelectListItem* ps4_version = new brls::SelectListItem("PS4 Version", + { "PS4 > 8", "7 < PS4 < 8", "PS4 < 7"}); + auto ps4_version_cb = [this, ps4_version](int result) + { + switch(result) + { + case 0: + this->remote_ps4_version = 8000000; + break; + case 1: + this->remote_ps4_version = 7000000; + break; + case 2: + this->remote_ps4_version = 6000000; + break; + } + }; + ps4_version->getValueSelectedEvent()->subscribe(ps4_version_cb); + add_host->addView(ps4_version); + + brls::ListItem* register_host = new brls::ListItem("Register"); + auto register_host_cb = [this](brls::View * view) + { + bool err = false; + if(this->remote_display_name.length() <= 0) + { + brls::Application::notify("No Display name defined"); + err = true; + } + + if(this->remote_addr.length() <= 0) + { + brls::Application::notify("No Remote address provided"); + err = true; + } + + if(this->remote_ps4_version < 0) + { + brls::Application::notify("No PS4 Version provided"); + err = true; + } + + if(err) + return; + + Host * host = this->settings->GetOrCreateHost(&this->remote_display_name); + host->host_addr = this->remote_addr; + host->system_version = this->remote_ps4_version; + + HostInterface::Register(this->io, host, this->settings); + }; + register_host->getClickEvent()->subscribe(register_host_cb); + + add_host->addView(register_host); +} + PSRemotePlay::PSRemotePlay(IO * io, Host * host) : io(io), host(host) { diff --git a/switch/src/settings.cpp b/switch/src/settings.cpp index ed8d222..3c443bc 100644 --- a/switch/src/settings.cpp +++ b/switch/src/settings.cpp @@ -259,12 +259,12 @@ void Settings::SetCPUOverclock(Host * host, std::string value) } #endif -std::string Settings::GetHostIPAddr(Host * host) +std::string Settings::GetHostAddr(Host * host) { if(host != nullptr) return host->host_addr; else - CHIAKI_LOGE(this->log, "Cannot GetHostIPAddr from nullptr host"); + CHIAKI_LOGE(this->log, "Cannot GetHostAddr from nullptr host"); return ""; } @@ -393,7 +393,7 @@ void Settings::ParseFile() config_file.open(this->filename, std::fstream::in); std::string line; std::string value; - bool rp_key_b, rp_regist_key_b, rp_key_type_b; + bool rp_key_b = false, rp_regist_key_b = false, rp_key_type_b = false; Host *current_host = nullptr; if(config_file.is_open()) { @@ -413,17 +413,16 @@ void Settings::ParseFile() // current host is in context current_host = this->GetOrCreateHost(&value); // all following case will edit the current_host config - break; - case HOST_IP: - CHIAKI_LOGV(this->log, "HOST_IP %s", value.c_str()); - if(current_host != nullptr) - current_host->host_addr = value; - // reset bool flags rp_key_b=false; rp_regist_key_b=false; rp_key_type_b=false; break; + case HOST_ADDR: + CHIAKI_LOGV(this->log, "HOST_ADDR %s", value.c_str()); + if(current_host != nullptr) + current_host->host_addr = value; + break; case PSN_ONLINE_ID: CHIAKI_LOGV(this->log, "PSN_ONLINE_ID %s", value.c_str()); // current_host == nullptr @@ -510,7 +509,7 @@ int Settings::WriteFile() CHIAKI_LOGD(this->log, "Write Host config file %s", it->first.c_str()); config_file << "[" << it->first << "]\n" - << "host_ip = \"" << it->second.host_addr << "\"\n"; + << "host_addr = \"" << it->second.host_addr << "\"\n"; if(it->second.video_resolution) config_file << "video_resolution = \"" From 7378b31bc18dd80c0ba49db6903acb6ff668c972 Mon Sep 17 00:00:00 2001 From: h0neybadger Date: Thu, 24 Dec 2020 19:55:05 +0100 Subject: [PATCH 109/237] Add switch PS5 ChiakiTarget config --- switch/include/gui.h | 2 +- switch/include/host.h | 53 ++++++++++---------- switch/include/settings.h | 5 ++ switch/src/discoverymanager.cpp | 22 +++----- switch/src/gui.cpp | 89 ++++++++++++++++++--------------- switch/src/host.cpp | 79 +++++++++++++++++++++++++---- switch/src/settings.cpp | 41 +++++++++++++-- 7 files changed, 193 insertions(+), 98 deletions(-) diff --git a/switch/include/gui.h b/switch/include/gui.h index 9ebbab4..246d743 100644 --- a/switch/include/gui.h +++ b/switch/include/gui.h @@ -56,7 +56,7 @@ class MainApplication // add_host local settings std::string remote_display_name = ""; std::string remote_addr = ""; - int remote_ps4_version = 8000000; + ChiakiTarget remote_ps_version = CHIAKI_TARGET_PS5_1; bool BuildConfigurationMenu(brls::List *, Host * host = nullptr); void BuildAddHostConfigurationMenu(brls::List *); diff --git a/switch/include/host.h b/switch/include/host.h index 26f3c8b..3696825 100644 --- a/switch/include/host.h +++ b/switch/include/host.h @@ -42,8 +42,6 @@ class Host //video config ChiakiVideoResolutionPreset video_resolution = CHIAKI_VIDEO_RESOLUTION_PRESET_720p; ChiakiVideoFPSPreset video_fps = CHIAKI_VIDEO_FPS_PRESET_60; - // info from discovery manager - int device_discovery_protocol_version = 0; std::string host_type; // user info std::string psn_online_id = ""; @@ -55,11 +53,24 @@ class Host std::function chiaki_regist_event_type_finished_failed = nullptr; std::function chiaki_regist_event_type_finished_success = nullptr; + // internal state + bool discovered = false; + bool registered = false; + // rp_key_data is true when rp_key, rp_regist_key, rp_key_type + bool rp_key_data = false; + + std::string host_name; + // sony's host_id == mac addr without colon + std::string host_id; + std::string host_addr; std::string ap_ssid; std::string ap_bssid; std::string ap_key; std::string ap_name; std::string server_nickname; + ChiakiTarget target = CHIAKI_TARGET_PS4_UNKNOWN; + ChiakiDiscoveryHostState state = CHIAKI_DISCOVERY_HOST_STATE_UNKNOWN; + // mac address = 48 bits uint8_t server_mac[6] = {0}; char rp_regist_key[CHIAKI_SESSION_AUTH_SIZE] = {0}; @@ -74,20 +85,8 @@ class Host friend class DiscoveryManager; friend class Settings; public: - // internal state - ChiakiDiscoveryHostState state = CHIAKI_DISCOVERY_HOST_STATE_UNKNOWN; - bool discovered = false; - bool registered = false; - // rp_key_data is true when rp_key, rp_regist_key, rp_key_type - bool rp_key_data = false; - std::string host_name; - int system_version = 0; - // sony's host_id == mac addr without colon - std::string host_id; - std::string host_addr; Host(ChiakiLog * log, Settings * settings, std::string host_name); ~Host(); - bool GetVideoResolution(int * ret_width, int * ret_height); int Register(std::string pin); int Wakeup(); int InitSession(IO *); @@ -96,19 +95,19 @@ class Host void StartSession(); void SendFeedbackState(ChiakiControllerState*); void RegistCB(ChiakiRegistEvent *); - - void SetRegistEventTypeFinishedCanceled(std::function chiaki_regist_event_type_finished_canceled) - { - this->chiaki_regist_event_type_finished_canceled = chiaki_regist_event_type_finished_canceled; - }; - void SetRegistEventTypeFinishedFailed(std::function chiaki_regist_event_type_finished_failed) - { - this->chiaki_regist_event_type_finished_failed = chiaki_regist_event_type_finished_failed; - }; - void SetRegistEventTypeFinishedSuccess(std::function chiaki_regist_event_type_finished_success) - { - this->chiaki_regist_event_type_finished_success = chiaki_regist_event_type_finished_success; - }; + bool GetVideoResolution(int * ret_width, int * ret_height); + std::string GetHostName(); + std::string GetHostAddr(); + ChiakiTarget GetChiakiTarget(); + void SetChiakiTarget(ChiakiTarget target); + void SetHostAddr(std::string host_addr); + void SetRegistEventTypeFinishedCanceled(std::function chiaki_regist_event_type_finished_canceled); + void SetRegistEventTypeFinishedFailed(std::function chiaki_regist_event_type_finished_failed); + void SetRegistEventTypeFinishedSuccess(std::function chiaki_regist_event_type_finished_success); + bool IsRegistered(); + bool IsDiscovered(); + bool IsReady(); + bool HasRPkey(); }; #endif diff --git a/switch/include/settings.h b/switch/include/settings.h index 176f864..0bdf7e0 100644 --- a/switch/include/settings.h +++ b/switch/include/settings.h @@ -36,6 +36,7 @@ class Settings RP_REGIST_KEY, VIDEO_RESOLUTION, VIDEO_FPS, + TARGET, } ConfigurationItem; // dummy parser implementation @@ -51,6 +52,7 @@ class Settings { RP_REGIST_KEY, std::regex("^\\s*rp_regist_key\\s*=\\s*\"?([\\w/=+]+)\"?") }, { VIDEO_RESOLUTION, std::regex("^\\s*video_resolution\\s*=\\s*\"?(1080p|720p|540p|360p)\"?") }, { VIDEO_FPS, std::regex("^\\s*video_fps\\s*=\\s*\"?(60|30)\"?") }, + { TARGET, std::regex("^\\s*target\\s*=\\s*\"?(\\d+)\"?") }, }; ConfigurationItem ParseLine(std::string * line, std::string * value); @@ -75,6 +77,8 @@ class Settings void SetVideoFPS(Host * host, ChiakiVideoFPSPreset value); void SetVideoResolution(Host * host, std::string value); void SetVideoFPS(Host * host, std::string value); + bool SetChiakiTarget(Host * host, ChiakiTarget target); + bool SetChiakiTarget(Host * host, std::string value); std::string GetHostAddr(Host * host); std::string GetHostName(Host * host); bool SetHostRPKeyType(Host * host, std::string value); @@ -83,6 +87,7 @@ class Settings std::string GetHostRPRegistKey(Host * host); bool SetHostRPKey(Host * host, std::string rp_key_b64); bool SetHostRPRegistKey(Host * host, std::string rp_regist_key_b64); + ChiakiTarget GetChiakiTarget(Host * host); void ParseFile(); int WriteFile(); }; diff --git a/switch/src/discoverymanager.cpp b/switch/src/discoverymanager.cpp index 0b8bb4d..e2204d5 100644 --- a/switch/src/discoverymanager.cpp +++ b/switch/src/discoverymanager.cpp @@ -186,29 +186,19 @@ void DiscoveryManager::DiscoveryCB(ChiakiDiscoveryHost * discovered_host) CHIAKI_LOGI(this->log, "--"); CHIAKI_LOGI(this->log, "Discovered Host:"); CHIAKI_LOGI(this->log, "State: %s", chiaki_discovery_host_state_string(discovered_host->state)); - /* - host attr - uint32_t host_addr; - int system_version; - int device_discovery_protocol_version; - std::string host_name; - std::string host_type; - std::string host_id; - */ + host->state = discovered_host->state; // add host ptr to list - if(discovered_host->system_version) + if(discovered_host->system_version && discovered_host->device_discovery_protocol_version) { // example: 07020001 - host->system_version = atoi(discovered_host->system_version); - CHIAKI_LOGI(this->log, "System Version: %s", discovered_host->system_version); - } + ChiakiTarget target = chiaki_discovery_host_system_version_target(discovered_host); + host->SetChiakiTarget(target); - if(discovered_host->device_discovery_protocol_version) - { - host->device_discovery_protocol_version = atoi(discovered_host->device_discovery_protocol_version); + CHIAKI_LOGI(this->log, "System Version: %s", discovered_host->system_version); CHIAKI_LOGI(this->log, "Device Discovery Protocol Version: %s", discovered_host->device_discovery_protocol_version); + CHIAKI_LOGI(this->log, "PlayStation ChiakiTarget Version: %d", target); } if(discovered_host->host_request_port) diff --git a/switch/src/gui.cpp b/switch/src/gui.cpp index 4ffecfc..afef09e 100644 --- a/switch/src/gui.cpp +++ b/switch/src/gui.cpp @@ -53,13 +53,15 @@ void HostInterface::Register(IO * io, Host * host, Settings * settings, std::fun // user must provide psn id for registration std::string account_id = settings->GetPSNAccountID(host); std::string online_id = settings->GetPSNOnlineID(host); - if(host->system_version >= 7000000 && account_id.length() <= 0) + ChiakiTarget target = host->GetChiakiTarget(); + + if(target >= CHIAKI_TARGET_PS4_9 && account_id.length() <= 0) { // PS4 firmware > 7.0 DIALOG(upaid, "Undefined PSN Account ID (Please configure a valid psn_account_id)"); return; } - else if( host->system_version < 7000000 && host->system_version > 0 && online_id.length() <= 0) + else if(target < CHIAKI_TARGET_PS4_9 && online_id.length() <= 0) { // use oline ID for ps4 < 7.0 DIALOG(upoid, "Undefined PSN Online ID (Please configure a valid psn_online_id)"); @@ -141,7 +143,7 @@ void HostInterface::Register() void HostInterface::Wakeup(brls::View * view) { - if(!this->host->rp_key_data) + if(!this->host->HasRPkey()) { // the host is not registered yet DIALOG(prypf, "Please register your PlayStation first"); @@ -163,7 +165,7 @@ void HostInterface::Wakeup(brls::View * view) void HostInterface::Connect(brls::View * view) { // check that all requirements are met - if(!this->host->discovered && !this->host->rp_key_data) + if(!this->host->IsDiscovered() && !this->host->HasRPkey()) { // at this point the host must be discovered or registered manually // to validate the system_version accuracy @@ -171,14 +173,14 @@ void HostInterface::Connect(brls::View * view) } // ignore state for remote hosts - if(this->host->discovered && this->host->state != CHIAKI_DISCOVERY_HOST_STATE_READY) + if(this->host->IsDiscovered() && this->host->IsReady()) { // host in standby mode DIALOG(ptoyp, "Please turn on your PlayStation"); return; } - if(!this->host->rp_key_data) + if(!this->host->HasRPkey()) { this->Register(); } @@ -309,14 +311,14 @@ bool MainApplication::Load() { // add host to the gui only if the host is registered or discovered if(this->host_menuitems.find(&it->second) == this->host_menuitems.end() - && (it->second.rp_key_data == true || it->second.discovered == true)) + && (it->second.HasRPkey() == true || it->second.IsDiscovered() == true)) { brls::List* new_host = new brls::List(); this->host_menuitems[&it->second] = new_host; // create host if udefined HostInterface host_menu = HostInterface(new_host, this->io, &it->second, this->settings); BuildConfigurationMenu(new_host, &it->second); - this->rootFrame->addTab(it->second.host_name.c_str(), new_host); + this->rootFrame->addTab(it->second.GetHostName().c_str(), new_host); } } } @@ -325,6 +327,26 @@ bool MainApplication::Load() bool MainApplication::BuildConfigurationMenu(brls::List * ls, Host * host) { + std::string psn_account_id_string = this->settings->GetPSNAccountID(host); + brls::ListItem* psn_account_id = new brls::ListItem("PSN Account ID", "PS5 or PS4 v7.0 and greater (base64 account_id)"); + psn_account_id->setValue(psn_account_id_string.c_str()); + auto psn_account_id_cb = [this, host, psn_account_id](brls::View * view) + { + char account_id[CHIAKI_PSN_ACCOUNT_ID_SIZE * 2] = {0}; + bool input = this->io->ReadUserKeyboard(account_id, sizeof(account_id)); + if(input) + { + // update gui + psn_account_id->setValue(account_id); + // push in setting + this->settings->SetPSNAccountID(host, account_id); + // write on disk + this->settings->WriteFile(); + } + }; + psn_account_id->getClickEvent()->subscribe(psn_account_id_cb); + ls->addView(psn_account_id); + std::string psn_online_id_string = this->settings->GetPSNOnlineID(host); brls::ListItem* psn_online_id = new brls::ListItem("PSN Online ID"); psn_online_id->setValue(psn_online_id_string.c_str()); @@ -345,26 +367,6 @@ bool MainApplication::BuildConfigurationMenu(brls::List * ls, Host * host) psn_online_id->getClickEvent()->subscribe(psn_online_id_cb); ls->addView(psn_online_id); - std::string psn_account_id_string = this->settings->GetPSNAccountID(host); - brls::ListItem* psn_account_id = new brls::ListItem("PSN Account ID", "v7.0 and greater"); - psn_account_id->setValue(psn_account_id_string.c_str()); - auto psn_account_id_cb = [this, host, psn_account_id](brls::View * view) - { - char account_id[CHIAKI_PSN_ACCOUNT_ID_SIZE * 2] = {0}; - bool input = this->io->ReadUserKeyboard(account_id, sizeof(account_id)); - if(input) - { - // update gui - psn_account_id->setValue(account_id); - // push in setting - this->settings->SetPSNAccountID(host, account_id); - // write on disk - this->settings->WriteFile(); - } - }; - psn_account_id->getClickEvent()->subscribe(psn_account_id_cb); - ls->addView(psn_account_id); - int value; ChiakiVideoResolutionPreset resolution_preset = this->settings->GetVideoResolution(host); switch(resolution_preset) @@ -517,25 +519,32 @@ void MainApplication::BuildAddHostConfigurationMenu(brls::List * add_host) // brls::ListItem* port = new brls::ListItem("Remote session port", "tcp/udp 9295"); // brls::ListItem* port = new brls::ListItem("Remote stream port", "udp 9296"); // brls::ListItem* port = new brls::ListItem("Remote Senkusha port", "udp 9297"); - brls::SelectListItem* ps4_version = new brls::SelectListItem("PS4 Version", - { "PS4 > 8", "7 < PS4 < 8", "PS4 < 7"}); - auto ps4_version_cb = [this, ps4_version](int result) + brls::SelectListItem* ps_version = new brls::SelectListItem("PlayStation Version", + { "PS5", "PS4 > 8", "7 < PS4 < 8", "PS4 < 7"}); + auto ps_version_cb = [this, ps_version](int result) { switch(result) { case 0: - this->remote_ps4_version = 8000000; + // ps5 v1 + this->remote_ps_version = CHIAKI_TARGET_PS5_1; break; case 1: - this->remote_ps4_version = 7000000; + // ps4 v8 + this->remote_ps_version = CHIAKI_TARGET_PS4_10; break; case 2: - this->remote_ps4_version = 6000000; + // ps4 v7 + this->remote_ps_version = CHIAKI_TARGET_PS4_9; + break; + case 3: + // ps4 v6 + this->remote_ps_version = CHIAKI_TARGET_PS4_8; break; } }; - ps4_version->getValueSelectedEvent()->subscribe(ps4_version_cb); - add_host->addView(ps4_version); + ps_version->getValueSelectedEvent()->subscribe(ps_version_cb); + add_host->addView(ps_version); brls::ListItem* register_host = new brls::ListItem("Register"); auto register_host_cb = [this](brls::View * view) @@ -553,9 +562,9 @@ void MainApplication::BuildAddHostConfigurationMenu(brls::List * add_host) err = true; } - if(this->remote_ps4_version < 0) + if(this->remote_ps_version < 0) { - brls::Application::notify("No PS4 Version provided"); + brls::Application::notify("No PlayStation Version provided"); err = true; } @@ -563,8 +572,8 @@ void MainApplication::BuildAddHostConfigurationMenu(brls::List * add_host) return; Host * host = this->settings->GetOrCreateHost(&this->remote_display_name); - host->host_addr = this->remote_addr; - host->system_version = this->remote_ps4_version; + host->SetHostAddr(this->remote_addr); + host->SetChiakiTarget(this->remote_ps_version); HostInterface::Register(this->io, host, this->settings); }; diff --git a/switch/src/host.cpp b/switch/src/host.cpp index 797c088..fb707c9 100644 --- a/switch/src/host.cpp +++ b/switch/src/host.cpp @@ -74,14 +74,13 @@ int Host::Register(std::string pin) { // use pin and accont_id to negociate secrets for session // - // convert psn_account_id into uint8_t[CHIAKI_PSN_ACCOUNT_ID_SIZE] - // CHIAKI_PSN_ACCOUNT_ID_SIZE == 8 std::string account_id = this->settings->GetPSNAccountID(this); std::string online_id = this->settings->GetPSNOnlineID(this); size_t account_id_size = sizeof(uint8_t[CHIAKI_PSN_ACCOUNT_ID_SIZE]); - // PS4 firmware > 7.0 - if(this->system_version >= 7000000) + regist_info.target = this->target; + + if(this->target >= CHIAKI_TARGET_PS4_9) { // use AccountID for ps4 > 7.0 if(account_id.length() > 0) @@ -95,12 +94,8 @@ int Host::Register(std::string pin) CHIAKI_LOGE(this->log, "Undefined PSN Account ID (Please configure a valid psn_account_id)"); return HOST_REGISTER_ERROR_SETTING_PSNACCOUNTID; } - if(this->system_version >= 8000000) - regist_info.target = CHIAKI_TARGET_PS4_10; - else - regist_info.target = CHIAKI_TARGET_PS4_9; } - else if( this->system_version < 7000000 && this->system_version > 0) + else if(this->target > CHIAKI_TARGET_PS4_UNKNOWN) { // use oline ID for ps4 < 7.0 if(online_id.length() > 0) @@ -113,7 +108,6 @@ int Host::Register(std::string pin) CHIAKI_LOGE(this->log, "Undefined PSN Online ID (Please configure a valid psn_online_id)"); return HOST_REGISTER_ERROR_SETTING_PSNONLINEID; } - regist_info.target = CHIAKI_TARGET_PS4_8; } else { @@ -147,6 +141,12 @@ int Host::InitSession(IO * user) chiaki_connect_info.host = this->host_addr.c_str(); chiaki_connect_info.video_profile = this->video_profile; + + if(this->target >= CHIAKI_TARGET_PS5_UNKNOWN) + chiaki_connect_info.ps5 = true; + else + chiaki_connect_info.ps5 = false; + memcpy(chiaki_connect_info.regist_key, this->rp_regist_key, sizeof(chiaki_connect_info.regist_key)); memcpy(chiaki_connect_info.morning, this->rp_key, sizeof(chiaki_connect_info.morning)); // set keybord state to 0 @@ -277,3 +277,62 @@ bool Host::GetVideoResolution(int * ret_width, int * ret_height) return true; } +std::string Host::GetHostName() +{ + return this->host_name; +} + +std::string Host::GetHostAddr() +{ + return this->host_addr; +} + +ChiakiTarget Host::GetChiakiTarget() +{ + return this->target; +} + +void Host::SetChiakiTarget(ChiakiTarget target) +{ + this->target = target; +} + +void Host::SetHostAddr(std::string host_addr) +{ + this->host_addr = host_addr; +} + +void Host::SetRegistEventTypeFinishedCanceled(std::function chiaki_regist_event_type_finished_canceled) +{ + this->chiaki_regist_event_type_finished_canceled = chiaki_regist_event_type_finished_canceled; +} + +void Host::SetRegistEventTypeFinishedFailed(std::function chiaki_regist_event_type_finished_failed) +{ + this->chiaki_regist_event_type_finished_failed = chiaki_regist_event_type_finished_failed; +} + +void Host::SetRegistEventTypeFinishedSuccess(std::function chiaki_regist_event_type_finished_success) +{ + this->chiaki_regist_event_type_finished_success = chiaki_regist_event_type_finished_success; +} + +bool Host::IsRegistered() +{ + return this->registered; +} + +bool Host::IsDiscovered() +{ + return this->discovered; +} + +bool Host::IsReady() +{ + return this->state == CHIAKI_DISCOVERY_HOST_STATE_READY; +} + +bool Host::HasRPkey() +{ + return this->rp_key_data; +} diff --git a/switch/src/settings.cpp b/switch/src/settings.cpp index 3c443bc..7f836e0 100644 --- a/switch/src/settings.cpp +++ b/switch/src/settings.cpp @@ -262,7 +262,7 @@ void Settings::SetCPUOverclock(Host * host, std::string value) std::string Settings::GetHostAddr(Host * host) { if(host != nullptr) - return host->host_addr; + return host->GetHostAddr(); else CHIAKI_LOGE(this->log, "Cannot GetHostAddr from nullptr host"); return ""; @@ -271,7 +271,7 @@ std::string Settings::GetHostAddr(Host * host) std::string Settings::GetHostName(Host * host) { if(host != nullptr) - return host->host_name; + return host->GetHostName(); else CHIAKI_LOGE(this->log, "Cannot GetHostName from nullptr host"); return ""; @@ -386,6 +386,31 @@ bool Settings::SetHostRPRegistKey(Host * host, std::string rp_regist_key_b64) return false; } +ChiakiTarget Settings::GetChiakiTarget(Host * host) +{ + return host->GetChiakiTarget(); +} + +bool Settings::SetChiakiTarget(Host * host, ChiakiTarget target) +{ + if(host != nullptr) + { + host->SetChiakiTarget(target); + return true; + } + else + { + CHIAKI_LOGE(this->log, "Cannot SetChiakiTarget from nullptr host"); + return false; + } +} + +bool Settings::SetChiakiTarget(Host * host, std::string value) +{ + // TODO Check possible target values + return this->SetChiakiTarget(host, static_cast(std::atoi(value.c_str()))); +} + void Settings::ParseFile() { CHIAKI_LOGI(this->log, "Parse config file %s", this->filename); @@ -456,6 +481,11 @@ void Settings::ParseFile() case VIDEO_FPS: this->SetVideoFPS(current_host, value); break; + case TARGET: + CHIAKI_LOGV(this->log, "TARGET %s", value.c_str()); + if(current_host != nullptr) + this->SetChiakiTarget(current_host, value); + break; } // ci switch if(rp_key_b && rp_regist_key_b && rp_key_type_b) // the current host contains rp key data @@ -509,8 +539,10 @@ int Settings::WriteFile() CHIAKI_LOGD(this->log, "Write Host config file %s", it->first.c_str()); config_file << "[" << it->first << "]\n" - << "host_addr = \"" << it->second.host_addr << "\"\n"; + << "host_addr = \"" << it->second.GetHostAddr() << "\"\n" + << "target = " << it->second.GetChiakiTarget() << "\"\n"; + config_file << "target = \"" << it->second.psn_account_id << "\"\n"; if(it->second.video_resolution) config_file << "video_resolution = \"" << this->ResolutionPresetToString(this->GetVideoResolution(&it->second)) @@ -535,7 +567,8 @@ int Settings::WriteFile() config_file << "rp_key = \"" << this->GetHostRPKey(&it->second) << "\"\n" << "rp_regist_key = \"" << this->GetHostRPRegistKey(&it->second) << "\"\n" << "rp_key_type = " << rp_key_type << "\n"; - } // + } + config_file << "\n"; } // for host } // is_open From 943e661af4b2a98a6bb4c0a8cb486e7849d1a4ef Mon Sep 17 00:00:00 2001 From: h0neybadger Date: Fri, 25 Dec 2020 10:08:36 +0100 Subject: [PATCH 110/237] Update switch gui HostInterface class to inherit from brls::List --- switch/include/gui.h | 8 +++----- switch/src/gui.cpp | 13 ++++++------- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/switch/include/gui.h b/switch/include/gui.h index 246d743..3a81833 100644 --- a/switch/include/gui.h +++ b/switch/include/gui.h @@ -19,17 +19,15 @@ #include "discoverymanager.h" #include "io.h" -class HostInterface +class HostInterface : public brls::List { private: - brls::TabFrame * root; IO * io; Host * host; Settings * settings; - brls::List * hostList; bool connected = false; public: - HostInterface(brls::List * hostList, IO * io, Host * host, Settings * settings); + HostInterface(IO * io, Host * host, Settings * settings); ~HostInterface(); static void Register(IO * io, Host * host, @@ -52,7 +50,7 @@ class MainApplication DiscoveryManager * discoverymanager; IO * io; brls::TabFrame * rootFrame; - std::map host_menuitems; + std::map host_menuitems; // add_host local settings std::string remote_display_name = ""; std::string remote_addr = ""; diff --git a/switch/src/gui.cpp b/switch/src/gui.cpp index afef09e..7193f9b 100644 --- a/switch/src/gui.cpp +++ b/switch/src/gui.cpp @@ -21,21 +21,21 @@ using namespace brls::i18n::literals; // for _i18n brls::Logger::info("Dialog: {0}", r); -HostInterface::HostInterface(brls::List * hostList, IO * io, Host * host, Settings * settings) - : hostList(hostList), io(io), host(host), settings(settings) +HostInterface::HostInterface(IO * io, Host * host, Settings * settings) + : io(io), host(host), settings(settings) { brls::ListItem* connect = new brls::ListItem("Connect"); connect->getClickEvent()->subscribe(std::bind(&HostInterface::Connect, this, std::placeholders::_1)); - this->hostList->addView(connect); + this->addView(connect); brls::ListItem* wakeup = new brls::ListItem("Wakeup"); wakeup->getClickEvent()->subscribe(std::bind(&HostInterface::Wakeup, this, std::placeholders::_1)); - this->hostList->addView(wakeup); + this->addView(wakeup); // message delimiter brls::Label* info = new brls::Label(brls::LabelStyle::REGULAR, "Host configuration", true); - this->hostList->addView(info); + this->addView(info); // push opengl chiaki stream // when the host is connected @@ -313,10 +313,9 @@ bool MainApplication::Load() if(this->host_menuitems.find(&it->second) == this->host_menuitems.end() && (it->second.HasRPkey() == true || it->second.IsDiscovered() == true)) { - brls::List* new_host = new brls::List(); + HostInterface * new_host = new HostInterface(this->io, &it->second, this->settings); this->host_menuitems[&it->second] = new_host; // create host if udefined - HostInterface host_menu = HostInterface(new_host, this->io, &it->second, this->settings); BuildConfigurationMenu(new_host, &it->second); this->rootFrame->addTab(it->second.GetHostName().c_str(), new_host); } From 05b22e15f12afa645c85f35d8f2119775afe0953 Mon Sep 17 00:00:00 2001 From: h0neybadger Date: Fri, 25 Dec 2020 18:43:20 +0100 Subject: [PATCH 111/237] Refactor switch Settings as singleton --- switch/include/gui.h | 7 +- switch/include/settings.h | 111 +++-- switch/src/gui.cpp | 13 +- switch/src/main.cpp | 34 +- switch/src/settings.cpp | 924 +++++++++++++++++++------------------- 5 files changed, 562 insertions(+), 527 deletions(-) diff --git a/switch/include/gui.h b/switch/include/gui.h index 3a81833..8af07ca 100644 --- a/switch/include/gui.h +++ b/switch/include/gui.h @@ -44,9 +44,8 @@ class HostInterface : public brls::List class MainApplication { private: - ChiakiLog * log; - std::map * hosts; Settings * settings; + ChiakiLog * log; DiscoveryManager * discoverymanager; IO * io; brls::TabFrame * rootFrame; @@ -59,9 +58,7 @@ class MainApplication bool BuildConfigurationMenu(brls::List *, Host * host = nullptr); void BuildAddHostConfigurationMenu(brls::List *); public: - MainApplication(std::map * hosts, - Settings * settings, DiscoveryManager * discoverymanager, - IO * io, ChiakiLog * log); + MainApplication(DiscoveryManager * discoverymanager, IO * io); ~MainApplication(); bool Load(); diff --git a/switch/include/settings.h b/switch/include/settings.h index 0bdf7e0..08ecbb2 100644 --- a/switch/include/settings.h +++ b/switch/include/settings.h @@ -5,19 +5,24 @@ #include -#include "host.h" #include +#include "host.h" // mutual host and settings class Host; class Settings { - private: - ChiakiLog * log; - const char * filename = "chiaki.conf"; + protected: + // keep constructor private (sigleton class) + Settings(); + static Settings * instance; + + private: + const char * filename = "chiaki.conf"; + ChiakiLog log; + std::map hosts; - std::map *hosts; // global_settings from psedo INI file ChiakiVideoResolutionPreset global_video_resolution = CHIAKI_VIDEO_RESOLUTION_PRESET_720p; ChiakiVideoFPSPreset global_video_fps = CHIAKI_VIDEO_FPS_PRESET_60; @@ -43,53 +48,71 @@ class Settings // the aim is not to have bulletproof parser // the goal is to read/write inernal flat configuration file const std::map re_map = { - { HOST_NAME, std::regex("^\\[\\s*(.+)\\s*\\]") }, - { HOST_ADDR, std::regex("^\\s*host_(?:ip|addr)\\s*=\\s*\"?((\\d+\\.\\d+\\.\\d+\\.\\d+)|([A-Za-z0-9-]{1,255}))\"?") }, - { PSN_ONLINE_ID, std::regex("^\\s*psn_online_id\\s*=\\s*\"?(\\w+)\"?") }, - { PSN_ACCOUNT_ID, std::regex("^\\s*psn_account_id\\s*=\\s*\"?([\\w/=+]+)\"?") }, - { RP_KEY, std::regex("^\\s*rp_key\\s*=\\s*\"?([\\w/=+]+)\"?") }, - { RP_KEY_TYPE, std::regex("^\\s*rp_key_type\\s*=\\s*\"?(\\d)\"?") }, - { RP_REGIST_KEY, std::regex("^\\s*rp_regist_key\\s*=\\s*\"?([\\w/=+]+)\"?") }, - { VIDEO_RESOLUTION, std::regex("^\\s*video_resolution\\s*=\\s*\"?(1080p|720p|540p|360p)\"?") }, - { VIDEO_FPS, std::regex("^\\s*video_fps\\s*=\\s*\"?(60|30)\"?") }, - { TARGET, std::regex("^\\s*target\\s*=\\s*\"?(\\d+)\"?") }, + {HOST_NAME, std::regex("^\\[\\s*(.+)\\s*\\]")}, + {HOST_ADDR, std::regex("^\\s*host_(?:ip|addr)\\s*=\\s*\"?((\\d+\\.\\d+\\.\\d+\\.\\d+)|([A-Za-z0-9-]{1,255}))\"?")}, + {PSN_ONLINE_ID, std::regex("^\\s*psn_online_id\\s*=\\s*\"?(\\w+)\"?")}, + {PSN_ACCOUNT_ID, std::regex("^\\s*psn_account_id\\s*=\\s*\"?([\\w/=+]+)\"?")}, + {RP_KEY, std::regex("^\\s*rp_key\\s*=\\s*\"?([\\w/=+]+)\"?")}, + {RP_KEY_TYPE, std::regex("^\\s*rp_key_type\\s*=\\s*\"?(\\d)\"?")}, + {RP_REGIST_KEY, std::regex("^\\s*rp_regist_key\\s*=\\s*\"?([\\w/=+]+)\"?")}, + {VIDEO_RESOLUTION, std::regex("^\\s*video_resolution\\s*=\\s*\"?(1080p|720p|540p|360p)\"?")}, + {VIDEO_FPS, std::regex("^\\s*video_fps\\s*=\\s*\"?(60|30)\"?")}, + {TARGET, std::regex("^\\s*target\\s*=\\s*\"?(\\d+)\"?")}, }; ConfigurationItem ParseLine(std::string * line, std::string * value); size_t GetB64encodeSize(size_t); + public: - Settings(ChiakiLog * log, std::map * hosts): log(log), hosts(hosts){}; + // singleton configuration + Settings(const Settings&) = delete; + void operator=(const Settings&) = delete; + static Settings * GetInstance(); + + ChiakiLog * GetLogger(); + std::map * GetHostsMap(); Host * GetOrCreateHost(std::string * host_name); - ChiakiLog* GetLogger(); - std::string GetPSNOnlineID(Host * host); - std::string GetPSNAccountID(Host * host); - void SetPSNOnlineID(Host * host, std::string psn_online_id); - void SetPSNAccountID(Host * host, std::string psn_account_id); - ChiakiVideoResolutionPreset GetVideoResolution(Host * host); - ChiakiVideoFPSPreset GetVideoFPS(Host * host); - std::string ResolutionPresetToString(ChiakiVideoResolutionPreset resolution); - std::string FPSPresetToString(ChiakiVideoFPSPreset fps); - ChiakiVideoResolutionPreset StringToResolutionPreset(std::string value); - ChiakiVideoFPSPreset StringToFPSPreset(std::string value); - int ResolutionPresetToInt(ChiakiVideoResolutionPreset resolution); - int FPSPresetToInt(ChiakiVideoFPSPreset fps); - void SetVideoResolution(Host * host, ChiakiVideoResolutionPreset value); - void SetVideoFPS(Host * host, ChiakiVideoFPSPreset value); - void SetVideoResolution(Host * host, std::string value); - void SetVideoFPS(Host * host, std::string value); - bool SetChiakiTarget(Host * host, ChiakiTarget target); - bool SetChiakiTarget(Host * host, std::string value); - std::string GetHostAddr(Host * host); - std::string GetHostName(Host * host); - bool SetHostRPKeyType(Host * host, std::string value); - int GetHostRPKeyType(Host * host); - std::string GetHostRPKey(Host * host); - std::string GetHostRPRegistKey(Host * host); - bool SetHostRPKey(Host * host, std::string rp_key_b64); - bool SetHostRPRegistKey(Host * host, std::string rp_regist_key_b64); - ChiakiTarget GetChiakiTarget(Host * host); + void ParseFile(); int WriteFile(); + + std::string ResolutionPresetToString(ChiakiVideoResolutionPreset resolution); + int ResolutionPresetToInt(ChiakiVideoResolutionPreset resolution); + ChiakiVideoResolutionPreset StringToResolutionPreset(std::string value); + + std::string FPSPresetToString(ChiakiVideoFPSPreset fps); + int FPSPresetToInt(ChiakiVideoFPSPreset fps); + ChiakiVideoFPSPreset StringToFPSPreset(std::string value); + + std::string GetHostName(Host * host); + std::string GetHostAddr(Host * host); + + std::string GetPSNOnlineID(Host * host); + void SetPSNOnlineID(Host * host, std::string psn_online_id); + + std::string GetPSNAccountID(Host * host); + void SetPSNAccountID(Host * host, std::string psn_account_id); + + ChiakiVideoResolutionPreset GetVideoResolution(Host * host); + void SetVideoResolution(Host * host, ChiakiVideoResolutionPreset value); + void SetVideoResolution(Host * host, std::string value); + + ChiakiVideoFPSPreset GetVideoFPS(Host * host); + void SetVideoFPS(Host * host, ChiakiVideoFPSPreset value); + void SetVideoFPS(Host * host, std::string value); + + ChiakiTarget GetChiakiTarget(Host * host); + bool SetChiakiTarget(Host * host, ChiakiTarget target); + bool SetChiakiTarget(Host * host, std::string value); + + std::string GetHostRPKey(Host * host); + bool SetHostRPKey(Host * host, std::string rp_key_b64); + + std::string GetHostRPRegistKey(Host * host); + bool SetHostRPRegistKey(Host * host, std::string rp_regist_key_b64); + + int GetHostRPKeyType(Host * host); + bool SetHostRPKeyType(Host * host, std::string value); }; #endif // CHIAKI_SETTINGS_H diff --git a/switch/src/gui.cpp b/switch/src/gui.cpp index 7193f9b..2d82fbd 100644 --- a/switch/src/gui.cpp +++ b/switch/src/gui.cpp @@ -246,12 +246,11 @@ bool HostInterface::CloseStream(ChiakiQuitEvent * quit) return false; } -MainApplication::MainApplication(std::map * hosts, - Settings * settings, DiscoveryManager * discoverymanager, - IO * io, ChiakiLog * log) - : hosts(hosts), settings(settings), discoverymanager(discoverymanager), - io(io), log(log) +MainApplication::MainApplication(DiscoveryManager * discoverymanager, IO * io) + : discoverymanager(discoverymanager), io(io) { + this->settings = Settings::GetInstance(); + this->log = this->settings->GetLogger(); } MainApplication::~MainApplication() @@ -305,9 +304,11 @@ bool MainApplication::Load() // Add the root view to the stack brls::Application::pushView(this->rootFrame); + + std::map * hosts = this->settings->GetHostsMap(); while(brls::Application::mainLoop()) { - for(auto it = this->hosts->begin(); it != this->hosts->end(); it++) + for(auto it = hosts->begin(); it != hosts->end(); it++) { // add host to the gui only if the host is registered or discovered if(this->host_menuitems.find(&it->second) == this->host_menuitems.end() diff --git a/switch/src/main.cpp b/switch/src/main.cpp index 8b0a682..bf5133a 100644 --- a/switch/src/main.cpp +++ b/switch/src/main.cpp @@ -107,49 +107,39 @@ extern "C" void userAppExit() int main(int argc, char* argv[]) { - // init chiaki lib - ChiakiLog log; -#if defined(__SWITCH__) && !defined(CHIAKI_ENABLE_SWITCH_NXLINK) - // null log for switch version - chiaki_log_init(&log, 0, chiaki_log_cb_print, NULL); -#else - chiaki_log_init(&log, CHIAKI_LOG_ALL ^ CHIAKI_LOG_VERBOSE, chiaki_log_cb_print, NULL); -#endif - // load chiaki lib - CHIAKI_LOGI(&log, "Loading chaki lib"); + Settings * settings = Settings::GetInstance(); + ChiakiLog * log = settings->GetLogger(); + + CHIAKI_LOGI(log, "Loading chaki lib"); ChiakiErrorCode err = chiaki_lib_init(); if(err != CHIAKI_ERR_SUCCESS) { - CHIAKI_LOGE(&log, "Chiaki lib init failed: %s\n", chiaki_error_string(err)); + CHIAKI_LOGE(log, "Chiaki lib init failed: %s\n", chiaki_error_string(err)); return 1; } - CHIAKI_LOGI(&log, "Loading SDL audio / joystick"); + CHIAKI_LOGI(log, "Loading SDL audio / joystick"); if(SDL_Init( SDL_INIT_AUDIO | SDL_INIT_JOYSTICK )) { - CHIAKI_LOGE(&log, "SDL initialization failed: %s", SDL_GetError()); + CHIAKI_LOGE(log, "SDL initialization failed: %s", SDL_GetError()); return 1; } // build sdl OpenGl and AV decoders graphical interface - IO io = IO(&log); // open Input Output class + IO io = IO(log); // open Input Output class - // manage ps4 setting discovery wakeup and registration - std::map hosts; // create host objects form config file - Settings settings = Settings(&log, &hosts); - CHIAKI_LOGI(&log, "Read chiaki settings file"); + CHIAKI_LOGI(log, "Read chiaki settings file"); // FIXME use GUI for config - settings.ParseFile(); Host * host = nullptr; - DiscoveryManager discoverymanager = DiscoveryManager(&settings); - MainApplication app = MainApplication(&hosts, &settings, &discoverymanager, &io, &log); + DiscoveryManager discoverymanager = DiscoveryManager(settings); + MainApplication app = MainApplication(&discoverymanager, &io); app.Load(); - CHIAKI_LOGI(&log, "Quit applet"); + CHIAKI_LOGI(log, "Quit applet"); SDL_Quit(); return 0; } diff --git a/switch/src/settings.cpp b/switch/src/settings.cpp index 7f836e0..3930a4e 100644 --- a/switch/src/settings.cpp +++ b/switch/src/settings.cpp @@ -4,33 +4,19 @@ #include #include "settings.h" -Host * Settings::GetOrCreateHost(std::string *host_name) +Settings::Settings() { - bool created = false; - // update of create Host instance - if(this->hosts->find(*host_name) == hosts->end()) - { - // create host if udefined - Host h = Host(this->log, this, *host_name); - this->hosts->emplace( *host_name, h); - created = true; - } - - Host *host = &(this->hosts->at(*host_name)); - if(created) - { - // copy default settings - // to the newly created host - this->SetPSNOnlineID(host, this->global_psn_online_id); - this->SetPSNAccountID(host, this->global_psn_account_id); - this->SetVideoResolution(host, this->global_video_resolution); - this->SetVideoFPS(host, this->global_video_fps); - } - return host; +#if defined(__SWITCH__) && !defined(CHIAKI_ENABLE_SWITCH_NXLINK) + // null log for switch version + chiaki_log_init(&this->log, 0, chiaki_log_cb_print, NULL); +#else + chiaki_log_init(&this->log, CHIAKI_LOG_ALL ^ CHIAKI_LOG_VERBOSE, chiaki_log_cb_print, NULL); +#endif + CHIAKI_LOGI(&this->log, "Read chiaki settings file %s", this->filename); + this->ParseFile(); } - -Settings::ConfigurationItem Settings::ParseLine(std::string *line, std::string *value) +Settings::ConfigurationItem Settings::ParseLine(std::string * line, std::string * value) { Settings::ConfigurationItem ci; std::smatch m; @@ -46,33 +32,334 @@ Settings::ConfigurationItem Settings::ParseLine(std::string *line, std::string * return UNKNOWN; } -ChiakiLog* Settings::GetLogger() -{ - return this->log; -} - size_t Settings::GetB64encodeSize(size_t in) { // calculate base64 buffer size after encode return ((4 * in / 3) + 3) & ~3; } +Settings * Settings::instance = nullptr; + +Settings * Settings::GetInstance() +{ + if(instance == nullptr) + instance = new Settings; + return instance; +} + +ChiakiLog * Settings::GetLogger() +{ + return &this->log; +} + +std::map * Settings::GetHostsMap() +{ + return &this->hosts; +} + +Host * Settings::GetOrCreateHost(std::string * host_name) +{ + bool created = false; + // update of create Host instance + if(this->hosts.find(*host_name) == hosts.end()) + { + // create host if udefined + Host h = Host(&this->log, this, *host_name); + this->hosts.emplace(*host_name, h); + created = true; + } + + Host * host = &(this->hosts.at(*host_name)); + if(created) + { + // copy default settings + // to the newly created host + this->SetPSNOnlineID(host, this->global_psn_online_id); + this->SetPSNAccountID(host, this->global_psn_account_id); + this->SetVideoResolution(host, this->global_video_resolution); + this->SetVideoFPS(host, this->global_video_fps); + } + return host; +} + +void Settings::ParseFile() +{ + CHIAKI_LOGI(&this->log, "Parse config file %s", this->filename); + std::fstream config_file; + config_file.open(this->filename, std::fstream::in); + std::string line; + std::string value; + bool rp_key_b = false, rp_regist_key_b = false, rp_key_type_b = false; + Host * current_host = nullptr; + if(config_file.is_open()) + { + CHIAKI_LOGV(&this->log, "Config file opened"); + Settings::ConfigurationItem ci; + while(getline(config_file, line)) + { + CHIAKI_LOGV(&this->log, "Parse config line `%s`", line.c_str()); + // for each line loop over config regex + ci = this->ParseLine(&line, &value); + switch(ci) + { + // got to next line + case UNKNOWN: + CHIAKI_LOGV(&this->log, "UNKNOWN config"); + break; + case HOST_NAME: + CHIAKI_LOGV(&this->log, "HOST_NAME %s", value.c_str()); + // current host is in context + current_host = this->GetOrCreateHost(&value); + // all following case will edit the current_host config + + rp_key_b = false; + rp_regist_key_b = false; + rp_key_type_b = false; + break; + case HOST_ADDR: + CHIAKI_LOGV(&this->log, "HOST_ADDR %s", value.c_str()); + if(current_host != nullptr) + current_host->host_addr = value; + break; + case PSN_ONLINE_ID: + CHIAKI_LOGV(&this->log, "PSN_ONLINE_ID %s", value.c_str()); + // current_host == nullptr + // means we are in global ini section + // update default setting + this->SetPSNOnlineID(current_host, value); + break; + case PSN_ACCOUNT_ID: + CHIAKI_LOGV(&this->log, "PSN_ACCOUNT_ID %s", value.c_str()); + this->SetPSNAccountID(current_host, value); + break; + case RP_KEY: + CHIAKI_LOGV(&this->log, "RP_KEY %s", value.c_str()); + if(current_host != nullptr) + rp_key_b = this->SetHostRPKey(current_host, value); + break; + case RP_KEY_TYPE: + CHIAKI_LOGV(&this->log, "RP_KEY_TYPE %s", value.c_str()); + if(current_host != nullptr) + // TODO Check possible rp_type values + rp_key_type_b = this->SetHostRPKeyType(current_host, value); + break; + case RP_REGIST_KEY: + CHIAKI_LOGV(&this->log, "RP_REGIST_KEY %s", value.c_str()); + if(current_host != nullptr) + rp_regist_key_b = this->SetHostRPRegistKey(current_host, value); + break; + case VIDEO_RESOLUTION: + this->SetVideoResolution(current_host, value); + break; + case VIDEO_FPS: + this->SetVideoFPS(current_host, value); + break; + case TARGET: + CHIAKI_LOGV(&this->log, "TARGET %s", value.c_str()); + if(current_host != nullptr) + this->SetChiakiTarget(current_host, value); + break; + } // ci switch + if(rp_key_b && rp_regist_key_b && rp_key_type_b) + // the current host contains rp key data + current_host->rp_key_data = true; + } // is_open + config_file.close(); + } + return; +} + +int Settings::WriteFile() +{ + std::fstream config_file; + CHIAKI_LOGI(&this->log, "Write config file %s", this->filename); + // flush file (trunc) + // the config file is completely overwritten + config_file.open(this->filename, std::fstream::out | std::ofstream::trunc); + std::string line; + std::string value; + + if(config_file.is_open()) + { + // save global settings + CHIAKI_LOGD(&this->log, "Write Global config file %s", this->filename); + + if(this->global_video_resolution) + config_file << "video_resolution = \"" + << this->ResolutionPresetToString(this->GetVideoResolution(nullptr)) + << "\"\n"; + + if(this->global_video_fps) + config_file << "video_fps = " + << this->FPSPresetToString(this->GetVideoFPS(nullptr)) + << "\n"; + + if(this->global_psn_online_id.length()) + config_file << "psn_online_id = \"" << this->global_psn_online_id << "\"\n"; + + if(this->global_psn_account_id.length()) + config_file << "psn_account_id = \"" << this->global_psn_account_id << "\"\n"; + + // write host config in file + // loop over all configured + for(auto it = this->hosts.begin(); it != this->hosts.end(); it++) + { + // first is std::string + // second is Host + CHIAKI_LOGD(&this->log, "Write Host config file %s", it->first.c_str()); + + config_file << "[" << it->first << "]\n" + << "host_addr = \"" << it->second.GetHostAddr() << "\"\n" + << "target = " << it->second.GetChiakiTarget() << "\"\n"; + + config_file << "target = \"" << it->second.psn_account_id << "\"\n"; + if(it->second.video_resolution) + config_file << "video_resolution = \"" + << this->ResolutionPresetToString(this->GetVideoResolution(&it->second)) + << "\"\n"; + + if(it->second.video_fps) + config_file << "video_fps = " + << this->FPSPresetToString(this->GetVideoFPS(&it->second)) + << "\n"; + + if(it->second.psn_online_id.length()) + config_file << "psn_online_id = \"" << it->second.psn_online_id << "\"\n"; + + if(it->second.psn_account_id.length()) + config_file << "psn_account_id = \"" << it->second.psn_account_id << "\"\n"; + + if(it->second.rp_key_data || it->second.registered) + { + char rp_key_type[33] = {0}; + snprintf(rp_key_type, sizeof(rp_key_type), "%d", it->second.rp_key_type); + // save registered rp key for auto login + config_file << "rp_key = \"" << this->GetHostRPKey(&it->second) << "\"\n" + << "rp_regist_key = \"" << this->GetHostRPRegistKey(&it->second) << "\"\n" + << "rp_key_type = " << rp_key_type << "\n"; + } + + config_file << "\n"; + } // for host + } // is_open + config_file.close(); + return 0; +} + +std::string Settings::ResolutionPresetToString(ChiakiVideoResolutionPreset resolution) +{ + switch(resolution) + { + case CHIAKI_VIDEO_RESOLUTION_PRESET_360p: + return "360p"; + case CHIAKI_VIDEO_RESOLUTION_PRESET_540p: + return "540p"; + case CHIAKI_VIDEO_RESOLUTION_PRESET_720p: + return "720p"; + case CHIAKI_VIDEO_RESOLUTION_PRESET_1080p: + return "1080p"; + } + return "UNKNOWN"; +} + +int Settings::ResolutionPresetToInt(ChiakiVideoResolutionPreset resolution) +{ + switch(resolution) + { + case CHIAKI_VIDEO_RESOLUTION_PRESET_360p: + return 360; + case CHIAKI_VIDEO_RESOLUTION_PRESET_540p: + return 540; + case CHIAKI_VIDEO_RESOLUTION_PRESET_720p: + return 720; + case CHIAKI_VIDEO_RESOLUTION_PRESET_1080p: + return 1080; + } + return 0; +} + +ChiakiVideoResolutionPreset Settings::StringToResolutionPreset(std::string value) +{ + if(value.compare("1080p") == 0) + return CHIAKI_VIDEO_RESOLUTION_PRESET_1080p; + else if(value.compare("720p") == 0) + return CHIAKI_VIDEO_RESOLUTION_PRESET_720p; + else if(value.compare("540p") == 0) + return CHIAKI_VIDEO_RESOLUTION_PRESET_540p; + else if(value.compare("360p") == 0) + return CHIAKI_VIDEO_RESOLUTION_PRESET_360p; + + // default + CHIAKI_LOGE(&this->log, "Unable to parse String resolution: %s", + value.c_str()); + + return CHIAKI_VIDEO_RESOLUTION_PRESET_720p; +} + +std::string Settings::FPSPresetToString(ChiakiVideoFPSPreset fps) +{ + switch(fps) + { + case CHIAKI_VIDEO_FPS_PRESET_30: + return "30"; + case CHIAKI_VIDEO_FPS_PRESET_60: + return "60"; + } + return "UNKNOWN"; +} + +int Settings::FPSPresetToInt(ChiakiVideoFPSPreset fps) +{ + switch(fps) + { + case CHIAKI_VIDEO_FPS_PRESET_30: + return 30; + case CHIAKI_VIDEO_FPS_PRESET_60: + return 60; + } + return 0; +} + +ChiakiVideoFPSPreset Settings::StringToFPSPreset(std::string value) +{ + if(value.compare("60") == 0) + return CHIAKI_VIDEO_FPS_PRESET_60; + else if(value.compare("30") == 0) + return CHIAKI_VIDEO_FPS_PRESET_30; + + // default + CHIAKI_LOGE(&this->log, "Unable to parse String fps: %s", + value.c_str()); + + return CHIAKI_VIDEO_FPS_PRESET_30; +} + +std::string Settings::GetHostName(Host * host) +{ + if(host != nullptr) + return host->GetHostName(); + else + CHIAKI_LOGE(&this->log, "Cannot GetHostName from nullptr host"); + return ""; +} + +std::string Settings::GetHostAddr(Host * host) +{ + if(host != nullptr) + return host->GetHostAddr(); + else + CHIAKI_LOGE(&this->log, "Cannot GetHostAddr from nullptr host"); + return ""; +} + std::string Settings::GetPSNOnlineID(Host * host) { - if(host == nullptr || host->psn_online_id.length() == 0 ) + if(host == nullptr || host->psn_online_id.length() == 0) return this->global_psn_online_id; else return host->psn_online_id; } -std::string Settings::GetPSNAccountID(Host * host) -{ - if(host == nullptr || host->psn_account_id.length() == 0 ) - return this->global_psn_account_id; - else - return host->psn_account_id; -} - void Settings::SetPSNOnlineID(Host * host, std::string psn_online_id) { if(host == nullptr) @@ -81,6 +368,14 @@ void Settings::SetPSNOnlineID(Host * host, std::string psn_online_id) host->psn_online_id = psn_online_id; } +std::string Settings::GetPSNAccountID(Host * host) +{ + if(host == nullptr || host->psn_account_id.length() == 0) + return this->global_psn_account_id; + else + return host->psn_account_id; +} + void Settings::SetPSNAccountID(Host * host, std::string psn_account_id) { if(host == nullptr) @@ -89,94 +384,6 @@ void Settings::SetPSNAccountID(Host * host, std::string psn_account_id) host->psn_account_id = psn_account_id; } -std::string Settings::ResolutionPresetToString(ChiakiVideoResolutionPreset resolution) -{ - switch(resolution) - { - case CHIAKI_VIDEO_RESOLUTION_PRESET_360p: - return "360p"; - case CHIAKI_VIDEO_RESOLUTION_PRESET_540p: - return "540p"; - case CHIAKI_VIDEO_RESOLUTION_PRESET_720p: - return "720p"; - case CHIAKI_VIDEO_RESOLUTION_PRESET_1080p: - return "1080p"; - } - return "UNKNOWN"; -} - -std::string Settings::FPSPresetToString(ChiakiVideoFPSPreset fps) -{ - switch(fps) - { - case CHIAKI_VIDEO_FPS_PRESET_30: - return "30"; - case CHIAKI_VIDEO_FPS_PRESET_60: - return "60"; - } - return "UNKNOWN"; -} - -ChiakiVideoResolutionPreset Settings::StringToResolutionPreset(std::string value) -{ - if (value.compare("1080p") == 0) - return CHIAKI_VIDEO_RESOLUTION_PRESET_1080p; - else if (value.compare("720p") == 0) - return CHIAKI_VIDEO_RESOLUTION_PRESET_720p; - else if (value.compare("540p") == 0) - return CHIAKI_VIDEO_RESOLUTION_PRESET_540p; - else if (value.compare("360p") == 0) - return CHIAKI_VIDEO_RESOLUTION_PRESET_360p; - - // default - CHIAKI_LOGE(this->log, "Unable to parse String resolution: %s", - value.c_str()); - - return CHIAKI_VIDEO_RESOLUTION_PRESET_720p; -} - -ChiakiVideoFPSPreset Settings::StringToFPSPreset(std::string value) -{ - if (value.compare("60") == 0) - return CHIAKI_VIDEO_FPS_PRESET_60; - else if (value.compare("30") == 0) - return CHIAKI_VIDEO_FPS_PRESET_30; - - // default - CHIAKI_LOGE(this->log, "Unable to parse String fps: %s", - value.c_str()); - - return CHIAKI_VIDEO_FPS_PRESET_30; -} - -int Settings::FPSPresetToInt(ChiakiVideoFPSPreset fps) -{ - switch(fps) - { - case CHIAKI_VIDEO_FPS_PRESET_30: - return 30; - case CHIAKI_VIDEO_FPS_PRESET_60: - return 60; - } - return 0; -} - -int Settings::ResolutionPresetToInt(ChiakiVideoResolutionPreset resolution) -{ - switch(resolution) - { - case CHIAKI_VIDEO_RESOLUTION_PRESET_360p: - return 360; - case CHIAKI_VIDEO_RESOLUTION_PRESET_540p: - return 540; - case CHIAKI_VIDEO_RESOLUTION_PRESET_720p: - return 720; - case CHIAKI_VIDEO_RESOLUTION_PRESET_1080p: - return 1080; - } - return 0; -} - ChiakiVideoResolutionPreset Settings::GetVideoResolution(Host * host) { if(host == nullptr) @@ -185,14 +392,6 @@ ChiakiVideoResolutionPreset Settings::GetVideoResolution(Host * host) return host->video_resolution; } -ChiakiVideoFPSPreset Settings::GetVideoFPS(Host * host) -{ - if(host == nullptr) - return this->global_video_fps; - else - return host->video_fps; -} - void Settings::SetVideoResolution(Host * host, ChiakiVideoResolutionPreset value) { if(host == nullptr) @@ -207,6 +406,14 @@ void Settings::SetVideoResolution(Host * host, std::string value) this->SetVideoResolution(host, p); } +ChiakiVideoFPSPreset Settings::GetVideoFPS(Host * host) +{ + if(host == nullptr) + return this->global_video_fps; + else + return host->video_fps; +} + void Settings::SetVideoFPS(Host * host, ChiakiVideoFPSPreset value) { if(host == nullptr) @@ -221,6 +428,139 @@ void Settings::SetVideoFPS(Host * host, std::string value) this->SetVideoFPS(host, p); } +ChiakiTarget Settings::GetChiakiTarget(Host * host) +{ + return host->GetChiakiTarget(); +} + +bool Settings::SetChiakiTarget(Host * host, ChiakiTarget target) +{ + if(host != nullptr) + { + host->SetChiakiTarget(target); + return true; + } + else + { + CHIAKI_LOGE(&this->log, "Cannot SetChiakiTarget from nullptr host"); + return false; + } +} + +bool Settings::SetChiakiTarget(Host * host, std::string value) +{ + // TODO Check possible target values + return this->SetChiakiTarget(host, static_cast(std::atoi(value.c_str()))); +} + +std::string Settings::GetHostRPKey(Host * host) +{ + if(host != nullptr) + { + if(host->rp_key_data || host->registered) + { + size_t rp_key_b64_sz = this->GetB64encodeSize(0x10); + char rp_key_b64[rp_key_b64_sz + 1] = {0}; + ChiakiErrorCode err; + err = chiaki_base64_encode( + host->rp_key, 0x10, + rp_key_b64, sizeof(rp_key_b64)); + + if(CHIAKI_ERR_SUCCESS == err) + return rp_key_b64; + else + CHIAKI_LOGE(&this->log, "Failed to encode rp_key to base64"); + } + } + else + CHIAKI_LOGE(&this->log, "Cannot GetHostRPKey from nullptr host"); + + return ""; +} + +bool Settings::SetHostRPKey(Host * host, std::string rp_key_b64) +{ + if(host != nullptr) + { + size_t rp_key_sz = sizeof(host->rp_key); + ChiakiErrorCode err = chiaki_base64_decode( + rp_key_b64.c_str(), rp_key_b64.length(), + host->rp_key, &rp_key_sz); + if(CHIAKI_ERR_SUCCESS != err) + CHIAKI_LOGE(&this->log, "Failed to parse RP_KEY %s (it must be a base64 encoded)", rp_key_b64.c_str()); + else + return true; + } + else + CHIAKI_LOGE(&this->log, "Cannot SetHostRPKey from nullptr host"); + + return false; +} + +std::string Settings::GetHostRPRegistKey(Host * host) +{ + if(host != nullptr) + { + if(host->rp_key_data || host->registered) + { + size_t rp_regist_key_b64_sz = this->GetB64encodeSize(CHIAKI_SESSION_AUTH_SIZE); + char rp_regist_key_b64[rp_regist_key_b64_sz + 1] = {0}; + ChiakiErrorCode err; + err = chiaki_base64_encode( + (uint8_t *)host->rp_regist_key, CHIAKI_SESSION_AUTH_SIZE, + rp_regist_key_b64, sizeof(rp_regist_key_b64)); + + if(CHIAKI_ERR_SUCCESS == err) + return rp_regist_key_b64; + else + CHIAKI_LOGE(&this->log, "Failed to encode rp_regist_key to base64"); + } + } + else + CHIAKI_LOGE(&this->log, "Cannot GetHostRPRegistKey from nullptr host"); + + return ""; +} + +bool Settings::SetHostRPRegistKey(Host * host, std::string rp_regist_key_b64) +{ + if(host != nullptr) + { + size_t rp_regist_key_sz = sizeof(host->rp_regist_key); + ChiakiErrorCode err = chiaki_base64_decode( + rp_regist_key_b64.c_str(), rp_regist_key_b64.length(), + (uint8_t *)host->rp_regist_key, &rp_regist_key_sz); + if(CHIAKI_ERR_SUCCESS != err) + CHIAKI_LOGE(&this->log, "Failed to parse RP_REGIST_KEY %s (it must be a base64 encoded)", rp_regist_key_b64.c_str()); + else + return true; + } + else + CHIAKI_LOGE(&this->log, "Cannot SetHostRPKey from nullptr host"); + + return false; +} + +int Settings::GetHostRPKeyType(Host * host) +{ + if(host != nullptr) + return host->rp_key_type; + + CHIAKI_LOGE(&this->log, "Cannot GetHostRPKeyType from nullptr host"); + return 0; +} + +bool Settings::SetHostRPKeyType(Host * host, std::string value) +{ + if(host != nullptr) + { + // TODO Check possible rp_type values + host->rp_key_type = std::atoi(value.c_str()); + return true; + } + return false; +} + #ifdef CHIAKI_ENABLE_SWITCH_OVERCLOCK int Settings::GetCPUOverclock(Host * host) { @@ -259,320 +599,4 @@ void Settings::SetCPUOverclock(Host * host, std::string value) } #endif -std::string Settings::GetHostAddr(Host * host) -{ - if(host != nullptr) - return host->GetHostAddr(); - else - CHIAKI_LOGE(this->log, "Cannot GetHostAddr from nullptr host"); - return ""; -} - -std::string Settings::GetHostName(Host * host) -{ - if(host != nullptr) - return host->GetHostName(); - else - CHIAKI_LOGE(this->log, "Cannot GetHostName from nullptr host"); - return ""; -} - -int Settings::GetHostRPKeyType(Host * host) -{ - if(host != nullptr) - return host->rp_key_type; - - CHIAKI_LOGE(this->log, "Cannot GetHostRPKeyType from nullptr host"); - return 0; -} - - -bool Settings::SetHostRPKeyType(Host * host, std::string value) -{ - if(host != nullptr) - { - // TODO Check possible rp_type values - host->rp_key_type = std::atoi(value.c_str()); - return true; - } - return false; -} - -std::string Settings::GetHostRPKey(Host * host) -{ - if(host != nullptr) - { - if(host->rp_key_data || host->registered) - { - size_t rp_key_b64_sz = this->GetB64encodeSize(0x10); - char rp_key_b64[rp_key_b64_sz + 1] = {0}; - ChiakiErrorCode err; - err = chiaki_base64_encode( - host->rp_key, 0x10, - rp_key_b64, sizeof(rp_key_b64)); - - if(CHIAKI_ERR_SUCCESS == err) - return rp_key_b64; - else - CHIAKI_LOGE(this->log, "Failed to encode rp_key to base64"); - } - } - else - CHIAKI_LOGE(this->log, "Cannot GetHostRPKey from nullptr host"); - - return ""; -} - -std::string Settings::GetHostRPRegistKey(Host * host) -{ - if(host != nullptr) - { - if(host->rp_key_data || host->registered) - { - size_t rp_regist_key_b64_sz = this->GetB64encodeSize(CHIAKI_SESSION_AUTH_SIZE); - char rp_regist_key_b64[rp_regist_key_b64_sz + 1] = {0}; - ChiakiErrorCode err; - err = chiaki_base64_encode( - (uint8_t *) host->rp_regist_key, CHIAKI_SESSION_AUTH_SIZE, - rp_regist_key_b64, sizeof(rp_regist_key_b64)); - - if(CHIAKI_ERR_SUCCESS == err) - return rp_regist_key_b64; - else - CHIAKI_LOGE(this->log, "Failed to encode rp_regist_key to base64"); - } - } - else - CHIAKI_LOGE(this->log, "Cannot GetHostRPRegistKey from nullptr host"); - - return ""; -} - -bool Settings::SetHostRPKey(Host * host, std::string rp_key_b64) -{ - if(host != nullptr) - { - size_t rp_key_sz = sizeof(host->rp_key); - ChiakiErrorCode err = chiaki_base64_decode( - rp_key_b64.c_str(), rp_key_b64.length(), - host->rp_key, &rp_key_sz); - if(CHIAKI_ERR_SUCCESS != err) - CHIAKI_LOGE(this->log, "Failed to parse RP_KEY %s (it must be a base64 encoded)", rp_key_b64.c_str()); - else - return true; - } - else - CHIAKI_LOGE(this->log, "Cannot SetHostRPKey from nullptr host"); - - return false; -} - -bool Settings::SetHostRPRegistKey(Host * host, std::string rp_regist_key_b64) -{ - if(host != nullptr) - { - size_t rp_regist_key_sz = sizeof(host->rp_regist_key); - ChiakiErrorCode err = chiaki_base64_decode( - rp_regist_key_b64.c_str(), rp_regist_key_b64.length(), - (uint8_t*) host->rp_regist_key, &rp_regist_key_sz); - if(CHIAKI_ERR_SUCCESS != err) - CHIAKI_LOGE(this->log, "Failed to parse RP_REGIST_KEY %s (it must be a base64 encoded)", rp_regist_key_b64.c_str()); - else - return true; - } - else - CHIAKI_LOGE(this->log, "Cannot SetHostRPKey from nullptr host"); - - return false; -} - -ChiakiTarget Settings::GetChiakiTarget(Host * host) -{ - return host->GetChiakiTarget(); -} - -bool Settings::SetChiakiTarget(Host * host, ChiakiTarget target) -{ - if(host != nullptr) - { - host->SetChiakiTarget(target); - return true; - } - else - { - CHIAKI_LOGE(this->log, "Cannot SetChiakiTarget from nullptr host"); - return false; - } -} - -bool Settings::SetChiakiTarget(Host * host, std::string value) -{ - // TODO Check possible target values - return this->SetChiakiTarget(host, static_cast(std::atoi(value.c_str()))); -} - -void Settings::ParseFile() -{ - CHIAKI_LOGI(this->log, "Parse config file %s", this->filename); - std::fstream config_file; - config_file.open(this->filename, std::fstream::in); - std::string line; - std::string value; - bool rp_key_b = false, rp_regist_key_b = false, rp_key_type_b = false; - Host *current_host = nullptr; - if(config_file.is_open()) - { - CHIAKI_LOGV(this->log, "Config file opened"); - Settings::ConfigurationItem ci; - while(getline(config_file, line)) - { - CHIAKI_LOGV(this->log, "Parse config line `%s`", line.c_str()); - // for each line loop over config regex - ci = this->ParseLine(&line, &value); - switch(ci) - { - // got to next line - case UNKNOWN: CHIAKI_LOGV(this->log, "UNKNOWN config"); break; - case HOST_NAME: - CHIAKI_LOGV(this->log, "HOST_NAME %s", value.c_str()); - // current host is in context - current_host = this->GetOrCreateHost(&value); - // all following case will edit the current_host config - - rp_key_b=false; - rp_regist_key_b=false; - rp_key_type_b=false; - break; - case HOST_ADDR: - CHIAKI_LOGV(this->log, "HOST_ADDR %s", value.c_str()); - if(current_host != nullptr) - current_host->host_addr = value; - break; - case PSN_ONLINE_ID: - CHIAKI_LOGV(this->log, "PSN_ONLINE_ID %s", value.c_str()); - // current_host == nullptr - // means we are in global ini section - // update default setting - this->SetPSNOnlineID(current_host, value); - break; - case PSN_ACCOUNT_ID: - CHIAKI_LOGV(this->log, "PSN_ACCOUNT_ID %s", value.c_str()); - this->SetPSNAccountID(current_host, value); - break; - case RP_KEY: - CHIAKI_LOGV(this->log, "RP_KEY %s", value.c_str()); - if(current_host != nullptr) - rp_key_b = this->SetHostRPKey(current_host, value); - break; - case RP_KEY_TYPE: - CHIAKI_LOGV(this->log, "RP_KEY_TYPE %s", value.c_str()); - if(current_host != nullptr) - // TODO Check possible rp_type values - rp_key_type_b = this->SetHostRPKeyType(current_host, value); - break; - case RP_REGIST_KEY: - CHIAKI_LOGV(this->log, "RP_REGIST_KEY %s", value.c_str()); - if(current_host != nullptr) - rp_regist_key_b = this->SetHostRPRegistKey(current_host, value); - break; - case VIDEO_RESOLUTION: - this->SetVideoResolution(current_host, value); - break; - case VIDEO_FPS: - this->SetVideoFPS(current_host, value); - break; - case TARGET: - CHIAKI_LOGV(this->log, "TARGET %s", value.c_str()); - if(current_host != nullptr) - this->SetChiakiTarget(current_host, value); - break; - } // ci switch - if(rp_key_b && rp_regist_key_b && rp_key_type_b) - // the current host contains rp key data - current_host->rp_key_data = true; - } // is_open - config_file.close(); - } - return; -} - -int Settings::WriteFile() -{ - std::fstream config_file; - CHIAKI_LOGI(this->log, "Write config file %s", this->filename); - // flush file (trunc) - // the config file is completely overwritten - config_file.open(this->filename, std::fstream::out | std::ofstream::trunc); - std::string line; - std::string value; - - if(this->hosts == nullptr) - return -1; - - if(config_file.is_open()) - { - // save global settings - CHIAKI_LOGD(this->log, "Write Global config file %s", this->filename); - - if(this->global_video_resolution) - config_file << "video_resolution = \"" - << this->ResolutionPresetToString(this->GetVideoResolution(nullptr)) - << "\"\n"; - - if(this->global_video_fps) - config_file << "video_fps = " - << this->FPSPresetToString(this->GetVideoFPS(nullptr)) - << "\n"; - - if(this->global_psn_online_id.length()) - config_file << "psn_online_id = \"" << this->global_psn_online_id << "\"\n"; - - if(this->global_psn_account_id.length()) - config_file << "psn_account_id = \"" << this->global_psn_account_id << "\"\n"; - - // write host config in file - // loop over all configured - for(auto it = this->hosts->begin(); it != this->hosts->end(); it++ ) - { - // first is std::string - // second is Host - CHIAKI_LOGD(this->log, "Write Host config file %s", it->first.c_str()); - - config_file << "[" << it->first << "]\n" - << "host_addr = \"" << it->second.GetHostAddr() << "\"\n" - << "target = " << it->second.GetChiakiTarget() << "\"\n"; - - config_file << "target = \"" << it->second.psn_account_id << "\"\n"; - if(it->second.video_resolution) - config_file << "video_resolution = \"" - << this->ResolutionPresetToString(this->GetVideoResolution(&it->second)) - << "\"\n"; - - if(it->second.video_fps) - config_file << "video_fps = " - << this->FPSPresetToString(this->GetVideoFPS(&it->second)) - << "\n"; - - if(it->second.psn_online_id.length()) - config_file << "psn_online_id = \"" << it->second.psn_online_id << "\"\n"; - - if(it->second.psn_account_id.length()) - config_file << "psn_account_id = \"" << it->second.psn_account_id << "\"\n"; - - if(it->second.rp_key_data || it->second.registered) - { - char rp_key_type[33] = { 0 }; - snprintf(rp_key_type, sizeof(rp_key_type), "%d", it->second.rp_key_type); - // save registered rp key for auto login - config_file << "rp_key = \"" << this->GetHostRPKey(&it->second) << "\"\n" - << "rp_regist_key = \"" << this->GetHostRPRegistKey(&it->second) << "\"\n" - << "rp_key_type = " << rp_key_type << "\n"; - } - - config_file << "\n"; - } // for host - } // is_open - config_file.close(); - return 0; -} From ad9b0f3d19c7d74dce31b0f6e88cb04b4d483b33 Mon Sep 17 00:00:00 2001 From: h0neybadger Date: Fri, 25 Dec 2020 20:06:21 +0100 Subject: [PATCH 112/237] Fix switch PS5 discovery & wakeup PORT --- switch/include/discoverymanager.h | 19 ++-- switch/include/host.h | 22 +++-- switch/src/discoverymanager.cpp | 40 ++++---- switch/src/host.cpp | 153 ++++++++++++++++-------------- switch/src/main.cpp | 25 ++--- switch/src/settings.cpp | 7 +- 6 files changed, 132 insertions(+), 134 deletions(-) diff --git a/switch/include/discoverymanager.h b/switch/include/discoverymanager.h index d1222ab..76c504d 100644 --- a/switch/include/discoverymanager.h +++ b/switch/include/discoverymanager.h @@ -6,25 +6,26 @@ #include #include -#include #include +#include #include "host.h" #include "settings.h" -static void Discovery(ChiakiDiscoveryHost*, void*); +static void Discovery(ChiakiDiscoveryHost *, void *); class DiscoveryManager { private: - Settings * settings; - ChiakiLog * log; + Settings * settings = nullptr; + ChiakiLog * log = nullptr; ChiakiDiscoveryService service; ChiakiDiscovery discovery; - struct sockaddr * host_addr; - size_t host_addr_len; + struct sockaddr * host_addr = nullptr; + size_t host_addr_len = 0; uint32_t GetIPv4BroadcastAddr(); bool service_enable; + public: typedef enum hoststate { @@ -34,13 +35,13 @@ class DiscoveryManager SHUTDOWN, } HostState; - DiscoveryManager(Settings *settings); + DiscoveryManager(); ~DiscoveryManager(); void SetService(bool); int Send(); - int Send(struct sockaddr *host_addr, size_t host_addr_len); + int Send(struct sockaddr * host_addr, size_t host_addr_len); int Send(const char *); - void DiscoveryCB(ChiakiDiscoveryHost*); + void DiscoveryCB(ChiakiDiscoveryHost *); }; #endif //CHIAKI_DISCOVERYMANAGER_H diff --git a/switch/include/host.h b/switch/include/host.h index 3696825..f0793eb 100644 --- a/switch/include/host.h +++ b/switch/include/host.h @@ -3,22 +3,22 @@ #ifndef CHIAKI_HOST_H #define CHIAKI_HOST_H -#include -#include #include +#include +#include -#include -#include -#include -#include #include +#include +#include +#include +#include #include "exception.h" #include "io.h" #include "settings.h" class DiscoveryManager; -static void Discovery(ChiakiDiscoveryHost*, void *); +static void Discovery(ChiakiDiscoveryHost *, void *); static void InitAudioCB(unsigned int channels, unsigned int rate, void * user); static bool VideoCB(uint8_t * buf, size_t buf_size, void * user); static void AudioCB(int16_t * buf, size_t samples_count, void * user); @@ -82,10 +82,11 @@ class Host ChiakiOpusDecoder opus_decoder; ChiakiConnectVideoProfile video_profile; ChiakiControllerState keyboard_state; - friend class DiscoveryManager; friend class Settings; + friend class DiscoveryManager; + public: - Host(ChiakiLog * log, Settings * settings, std::string host_name); + Host(std::string host_name); ~Host(); int Register(std::string pin); int Wakeup(); @@ -93,7 +94,7 @@ class Host int FiniSession(); void StopSession(); void StartSession(); - void SendFeedbackState(ChiakiControllerState*); + void SendFeedbackState(ChiakiControllerState *); void RegistCB(ChiakiRegistEvent *); bool GetVideoResolution(int * ret_width, int * ret_height); std::string GetHostName(); @@ -108,6 +109,7 @@ class Host bool IsDiscovered(); bool IsReady(); bool HasRPkey(); + bool IsPS5(); }; #endif diff --git a/switch/src/discoverymanager.cpp b/switch/src/discoverymanager.cpp index e2204d5..b5f8b8c 100644 --- a/switch/src/discoverymanager.cpp +++ b/switch/src/discoverymanager.cpp @@ -4,32 +4,31 @@ #include #endif +#include #include +#include #include #include -#include -#include #include -#define PING_MS 500 -#define HOSTS_MAX 16 -#define DROP_PINGS 3 +#define PING_MS 500 +#define HOSTS_MAX 16 +#define DROP_PINGS 3 static void Discovery(ChiakiDiscoveryHost * discovered_hosts, size_t hosts_count, void * user) { - DiscoveryManager * dm = (DiscoveryManager *) user; - for(size_t i=0; i < hosts_count; i++) + DiscoveryManager * dm = (DiscoveryManager *)user; + for(size_t i = 0; i < hosts_count; i++) { - dm->DiscoveryCB(discovered_hosts+i); + dm->DiscoveryCB(discovered_hosts + i); } } - -DiscoveryManager::DiscoveryManager(Settings *settings) - : settings(settings), host_addr(nullptr), host_addr_len(0) +DiscoveryManager::DiscoveryManager() { - this->log = this->settings->GetLogger(); + this->settings = Settings::GetInstance(); + this->log = this->settings->GetLogger(); } DiscoveryManager::~DiscoveryManager() @@ -87,12 +86,12 @@ uint32_t DiscoveryManager::GetIPv4BroadcastAddr() uint32_t current_addr, subnet_mask; // init nintendo net interface service Result rc = nifmInitialize(NifmServiceType_User); - if (R_SUCCEEDED(rc)) + if(R_SUCCEEDED(rc)) { // read current IP and netmask rc = nifmGetCurrentIpConfigInfo( - ¤t_addr, &subnet_mask, - NULL, NULL, NULL); + ¤t_addr, &subnet_mask, + NULL, NULL, NULL); nifmExit(); } else @@ -106,15 +105,13 @@ uint32_t DiscoveryManager::GetIPv4BroadcastAddr() #endif } -int DiscoveryManager::Send(struct sockaddr *host_addr, size_t host_addr_len) +int DiscoveryManager::Send(struct sockaddr * host_addr, size_t host_addr_len) { if(!host_addr) { CHIAKI_LOGE(log, "Null sockaddr"); return 1; } - ((struct sockaddr_in *)host_addr)->sin_port = htons(CHIAKI_DISCOVERY_PORT); - ChiakiDiscoveryPacket packet; memset(&packet, 0, sizeof(packet)); packet.cmd = CHIAKI_DISCOVERY_CMD_SRCH; @@ -136,7 +133,7 @@ int DiscoveryManager::Send(const char * discover_ip_dest) struct sockaddr * host_addr = nullptr; socklen_t host_addr_len = 0; - for(struct addrinfo *ai=host_addrinfos; ai; ai=ai->ai_next) + for(struct addrinfo * ai = host_addrinfos; ai; ai = ai->ai_next) { if(ai->ai_protocol != IPPROTO_UDP) continue; @@ -165,7 +162,6 @@ int DiscoveryManager::Send() struct sockaddr_in addr; addr.sin_family = AF_INET; addr.sin_addr.s_addr = GetIPv4BroadcastAddr(); - addr.sin_port = htons(CHIAKI_DISCOVERY_PORT); this->host_addr_len = sizeof(sockaddr_in); this->host_addr = (struct sockaddr *)malloc(host_addr_len); @@ -174,14 +170,13 @@ int DiscoveryManager::Send() return DiscoveryManager::Send(this->host_addr, this->host_addr_len); } - void DiscoveryManager::DiscoveryCB(ChiakiDiscoveryHost * discovered_host) { // the user ptr is passed as // chiaki_discovery_thread_start arg std::string key = discovered_host->host_name; - Host *host = this->settings->GetOrCreateHost(&key); + Host * host = this->settings->GetOrCreateHost(&key); CHIAKI_LOGI(this->log, "--"); CHIAKI_LOGI(this->log, "Discovered Host:"); @@ -230,4 +225,3 @@ void DiscoveryManager::DiscoveryCB(ChiakiDiscoveryHost * discovered_host) CHIAKI_LOGI(this->log, "--"); } - diff --git a/switch/src/host.cpp b/switch/src/host.cpp index fb707c9..204a967 100644 --- a/switch/src/host.cpp +++ b/switch/src/host.cpp @@ -1,47 +1,47 @@ // SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL - #include #include -#include "io.h" #include "host.h" - +#include "io.h" static void InitAudioCB(unsigned int channels, unsigned int rate, void * user) { - IO * io = (IO *) user; + IO * io = (IO *)user; io->InitAudioCB(channels, rate); } static bool VideoCB(uint8_t * buf, size_t buf_size, void * user) { - IO * io = (IO *) user; + IO * io = (IO *)user; return io->VideoCB(buf, buf_size); } static void AudioCB(int16_t * buf, size_t samples_count, void * user) { - IO * io = (IO *) user; + IO * io = (IO *)user; io->AudioCB(buf, samples_count); } static void EventCB(ChiakiEvent * event, void * user) { - IO * io = (IO *) user; + IO * io = (IO *)user; io->EventCB(event); } static void RegistEventCB(ChiakiRegistEvent * event, void * user) { - Host * host = (Host *) user; + Host * host = (Host *)user; host->RegistCB(event); } -Host::Host(ChiakiLog * log, Settings * settings, std::string host_name) - : log(log), settings(settings), host_name(host_name) +Host::Host(std::string host_name) + : host_name(host_name) { + this->settings = Settings::GetInstance(); + this->log = settings->GetLogger(); } Host::~Host() @@ -55,14 +55,16 @@ int Host::Wakeup() CHIAKI_LOGE(this->log, "Given registkey is too long"); return 1; } - else if (strlen(this->rp_regist_key) <=0) + else if(strlen(this->rp_regist_key) <= 0) { CHIAKI_LOGE(this->log, "Given registkey is not defined"); return 2; } uint64_t credential = (uint64_t)strtoull(this->rp_regist_key, NULL, 16); - ChiakiErrorCode ret = chiaki_discovery_wakeup(this->log, NULL, host_addr.c_str(), credential); + ChiakiErrorCode ret = chiaki_discovery_wakeup(this->log, NULL, + host_addr.c_str(), credential, this->IsPS5()); + if(ret == CHIAKI_ERR_SUCCESS) { //FIXME @@ -86,7 +88,7 @@ int Host::Register(std::string pin) if(account_id.length() > 0) { chiaki_base64_decode(account_id.c_str(), account_id.length(), - regist_info.psn_account_id, &(account_id_size)); + regist_info.psn_account_id, &(account_id_size)); regist_info.psn_online_id = nullptr; } else @@ -119,7 +121,7 @@ int Host::Register(std::string pin) this->regist_info.host = this->host_addr.c_str(); this->regist_info.broadcast = false; - if(this->system_version >= 7000000) + if(this->target >= CHIAKI_TARGET_PS4_9) CHIAKI_LOGI(this->log, "Registering to host `%s` `%s` with PSN AccountID `%s` pin `%s`", this->host_name.c_str(), this->host_addr.c_str(), account_id.c_str(), pin.c_str()); else @@ -133,19 +135,16 @@ int Host::Register(std::string pin) int Host::InitSession(IO * user) { chiaki_connect_video_profile_preset(&(this->video_profile), - this->video_resolution, this->video_fps); + this->video_resolution, this->video_fps); // Build chiaki ps4 stream session chiaki_opus_decoder_init(&(this->opus_decoder), this->log); ChiakiAudioSink audio_sink; - ChiakiConnectInfo chiaki_connect_info = { 0 }; + ChiakiConnectInfo chiaki_connect_info = {0}; chiaki_connect_info.host = this->host_addr.c_str(); chiaki_connect_info.video_profile = this->video_profile; - if(this->target >= CHIAKI_TARGET_PS5_UNKNOWN) - chiaki_connect_info.ps5 = true; - else - chiaki_connect_info.ps5 = false; + chiaki_connect_info.ps5 = this->IsPS5(); memcpy(chiaki_connect_info.regist_key, this->rp_regist_key, sizeof(chiaki_connect_info.regist_key)); memcpy(chiaki_connect_info.morning, this->rp_key, sizeof(chiaki_connect_info.morning)); @@ -208,43 +207,43 @@ void Host::RegistCB(ChiakiRegistEvent * event) this->registered = false; switch(event->type) { - case CHIAKI_REGIST_EVENT_TYPE_FINISHED_CANCELED: - CHIAKI_LOGI(this->log, "Register event CHIAKI_REGIST_EVENT_TYPE_FINISHED_CANCELED"); - if(this->chiaki_regist_event_type_finished_canceled != nullptr) - { - this->chiaki_regist_event_type_finished_canceled(); - } - break; - case CHIAKI_REGIST_EVENT_TYPE_FINISHED_FAILED: - CHIAKI_LOGI(this->log, "Register event CHIAKI_REGIST_EVENT_TYPE_FINISHED_FAILED"); - if(this->chiaki_regist_event_type_finished_failed != nullptr) - { - this->chiaki_regist_event_type_finished_failed(); - } - break; - case CHIAKI_REGIST_EVENT_TYPE_FINISHED_SUCCESS: - { - ChiakiRegisteredHost *r_host = event->registered_host; - CHIAKI_LOGI(this->log, "Register event CHIAKI_REGIST_EVENT_TYPE_FINISHED_SUCCESS"); - // copy values form ChiakiRegisteredHost object - this->ap_ssid = r_host->ap_ssid; - this->ap_key = r_host->ap_key; - this->ap_name = r_host->ap_name; - memcpy( &(this->server_mac), &(r_host->server_mac), sizeof(this->server_mac) ); - this->server_nickname = r_host->server_nickname; - memcpy( &(this->rp_regist_key), &(r_host->rp_regist_key), sizeof(this->rp_regist_key) ); - this->rp_key_type = r_host->rp_key_type; - memcpy( &(this->rp_key), &(r_host->rp_key), sizeof(this->rp_key) ); - // mark host as registered - this->registered = true; - this->rp_key_data = true; - CHIAKI_LOGI(this->log, "Register Success %s", this->host_name.c_str()); + case CHIAKI_REGIST_EVENT_TYPE_FINISHED_CANCELED: + CHIAKI_LOGI(this->log, "Register event CHIAKI_REGIST_EVENT_TYPE_FINISHED_CANCELED"); + if(this->chiaki_regist_event_type_finished_canceled != nullptr) + { + this->chiaki_regist_event_type_finished_canceled(); + } + break; + case CHIAKI_REGIST_EVENT_TYPE_FINISHED_FAILED: + CHIAKI_LOGI(this->log, "Register event CHIAKI_REGIST_EVENT_TYPE_FINISHED_FAILED"); + if(this->chiaki_regist_event_type_finished_failed != nullptr) + { + this->chiaki_regist_event_type_finished_failed(); + } + break; + case CHIAKI_REGIST_EVENT_TYPE_FINISHED_SUCCESS: + { + ChiakiRegisteredHost * r_host = event->registered_host; + CHIAKI_LOGI(this->log, "Register event CHIAKI_REGIST_EVENT_TYPE_FINISHED_SUCCESS"); + // copy values form ChiakiRegisteredHost object + this->ap_ssid = r_host->ap_ssid; + this->ap_key = r_host->ap_key; + this->ap_name = r_host->ap_name; + memcpy(&(this->server_mac), &(r_host->server_mac), sizeof(this->server_mac)); + this->server_nickname = r_host->server_nickname; + memcpy(&(this->rp_regist_key), &(r_host->rp_regist_key), sizeof(this->rp_regist_key)); + this->rp_key_type = r_host->rp_key_type; + memcpy(&(this->rp_key), &(r_host->rp_key), sizeof(this->rp_key)); + // mark host as registered + this->registered = true; + this->rp_key_data = true; + CHIAKI_LOGI(this->log, "Register Success %s", this->host_name.c_str()); - if(this->chiaki_regist_event_type_finished_success != nullptr) - this->chiaki_regist_event_type_finished_success(); + if(this->chiaki_regist_event_type_finished_success != nullptr) + this->chiaki_regist_event_type_finished_success(); - break; - } + break; + } } // close registration socket chiaki_regist_stop(&this->regist); @@ -255,24 +254,24 @@ bool Host::GetVideoResolution(int * ret_width, int * ret_height) { switch(this->video_resolution) { - case CHIAKI_VIDEO_RESOLUTION_PRESET_360p: - *ret_width = 640; - *ret_height = 360; - break; - case CHIAKI_VIDEO_RESOLUTION_PRESET_540p: - *ret_width = 950; - *ret_height = 540; - break; - case CHIAKI_VIDEO_RESOLUTION_PRESET_720p: - *ret_width = 1280; - *ret_height = 720; - break; - case CHIAKI_VIDEO_RESOLUTION_PRESET_1080p: - *ret_width = 1920; - *ret_height = 1080; - break; - default: - return false; + case CHIAKI_VIDEO_RESOLUTION_PRESET_360p: + *ret_width = 640; + *ret_height = 360; + break; + case CHIAKI_VIDEO_RESOLUTION_PRESET_540p: + *ret_width = 950; + *ret_height = 540; + break; + case CHIAKI_VIDEO_RESOLUTION_PRESET_720p: + *ret_width = 1280; + *ret_height = 720; + break; + case CHIAKI_VIDEO_RESOLUTION_PRESET_1080p: + *ret_width = 1920; + *ret_height = 1080; + break; + default: + return false; } return true; } @@ -336,3 +335,11 @@ bool Host::HasRPkey() { return this->rp_key_data; } + +bool Host::IsPS5() +{ + if(this->target >= CHIAKI_TARGET_PS5_UNKNOWN) + return true; + else + return false; +} diff --git a/switch/src/main.cpp b/switch/src/main.cpp index bf5133a..562f362 100644 --- a/switch/src/main.cpp +++ b/switch/src/main.cpp @@ -1,15 +1,15 @@ // SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL // chiaki modules -#include #include +#include // discover and wakeup ps4 host // from local network #include "discoverymanager.h" -#include "settings.h" -#include "io.h" #include "gui.h" +#include "io.h" +#include "settings.h" #ifdef __SWITCH__ #include @@ -51,11 +51,11 @@ static int s_nxlinkSock = -1; static void initNxLink() { // use chiaki socket config initialization - if (R_FAILED(socketInitialize(&g_chiakiSocketInitConfig))) + if(R_FAILED(socketInitialize(&g_chiakiSocketInitConfig))) return; s_nxlinkSock = nxlinkStdio(); - if (s_nxlinkSock >= 0) + if(s_nxlinkSock >= 0) printf("initNxLink"); else socketExit(); @@ -63,7 +63,7 @@ static void initNxLink() static void deinitNxLink() { - if (s_nxlinkSock >= 0) + if(s_nxlinkSock >= 0) { close(s_nxlinkSock); s_nxlinkSock = -1; @@ -105,7 +105,7 @@ extern "C" void userAppExit() } #endif // __SWITCH__ -int main(int argc, char* argv[]) +int main(int argc, char * argv[]) { // load chiaki lib Settings * settings = Settings::GetInstance(); @@ -121,7 +121,7 @@ int main(int argc, char* argv[]) } CHIAKI_LOGI(log, "Loading SDL audio / joystick"); - if(SDL_Init( SDL_INIT_AUDIO | SDL_INIT_JOYSTICK )) + if(SDL_Init(SDL_INIT_AUDIO | SDL_INIT_JOYSTICK)) { CHIAKI_LOGE(log, "SDL initialization failed: %s", SDL_GetError()); return 1; @@ -129,13 +129,7 @@ int main(int argc, char* argv[]) // build sdl OpenGl and AV decoders graphical interface IO io = IO(log); // open Input Output class - - // create host objects form config file - CHIAKI_LOGI(log, "Read chiaki settings file"); - // FIXME use GUI for config - Host * host = nullptr; - - DiscoveryManager discoverymanager = DiscoveryManager(settings); + DiscoveryManager discoverymanager = DiscoveryManager(); MainApplication app = MainApplication(&discoverymanager, &io); app.Load(); @@ -143,4 +137,3 @@ int main(int argc, char* argv[]) SDL_Quit(); return 0; } - diff --git a/switch/src/settings.cpp b/switch/src/settings.cpp index 3930a4e..3488f6b 100644 --- a/switch/src/settings.cpp +++ b/switch/src/settings.cpp @@ -12,8 +12,6 @@ Settings::Settings() #else chiaki_log_init(&this->log, CHIAKI_LOG_ALL ^ CHIAKI_LOG_VERBOSE, chiaki_log_cb_print, NULL); #endif - CHIAKI_LOGI(&this->log, "Read chiaki settings file %s", this->filename); - this->ParseFile(); } Settings::ConfigurationItem Settings::ParseLine(std::string * line, std::string * value) @@ -43,7 +41,10 @@ Settings * Settings::instance = nullptr; Settings * Settings::GetInstance() { if(instance == nullptr) + { instance = new Settings; + instance->ParseFile(); + } return instance; } @@ -64,7 +65,7 @@ Host * Settings::GetOrCreateHost(std::string * host_name) if(this->hosts.find(*host_name) == hosts.end()) { // create host if udefined - Host h = Host(&this->log, this, *host_name); + Host h = Host(*host_name); this->hosts.emplace(*host_name, h); created = true; } From 6fec40125232253b9cca9405c3db78c01a721639 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Sat, 26 Dec 2020 11:32:58 +0100 Subject: [PATCH 113/237] Update discovered state in DiscoveryManager --- switch/src/discoverymanager.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/switch/src/discoverymanager.cpp b/switch/src/discoverymanager.cpp index b5f8b8c..7f71578 100644 --- a/switch/src/discoverymanager.cpp +++ b/switch/src/discoverymanager.cpp @@ -183,6 +183,7 @@ void DiscoveryManager::DiscoveryCB(ChiakiDiscoveryHost * discovered_host) CHIAKI_LOGI(this->log, "State: %s", chiaki_discovery_host_state_string(discovered_host->state)); host->state = discovered_host->state; + host->discovered = true; // add host ptr to list if(discovered_host->system_version && discovered_host->device_discovery_protocol_version) From a1b081bfce66df328fb0aab5138bcbfee154cea5 Mon Sep 17 00:00:00 2001 From: h0neybadger Date: Sat, 26 Dec 2020 22:53:34 +0100 Subject: [PATCH 114/237] Fix switch GUI multiple hosts SetEventConnectedCallback --- switch/include/host.h | 1 - switch/src/gui.cpp | 13 +++++++------ switch/src/host.cpp | 2 -- switch/src/io.cpp | 6 ++++-- switch/src/main.cpp | 8 +++++++- switch/src/settings.cpp | 2 +- 6 files changed, 19 insertions(+), 13 deletions(-) diff --git a/switch/include/host.h b/switch/include/host.h index f0793eb..1966c1e 100644 --- a/switch/include/host.h +++ b/switch/include/host.h @@ -81,7 +81,6 @@ class Host ChiakiSession session; ChiakiOpusDecoder opus_decoder; ChiakiConnectVideoProfile video_profile; - ChiakiControllerState keyboard_state; friend class Settings; friend class DiscoveryManager; diff --git a/switch/src/gui.cpp b/switch/src/gui.cpp index 2d82fbd..3755cfe 100644 --- a/switch/src/gui.cpp +++ b/switch/src/gui.cpp @@ -37,10 +37,6 @@ HostInterface::HostInterface(IO * io, Host * host, Settings * settings) "Host configuration", true); this->addView(info); - // push opengl chiaki stream - // when the host is connected - this->io->SetEventConnectedCallback(std::bind(&HostInterface::Stream, this)); - this->io->SetEventQuitCallback(std::bind(&HostInterface::CloseStream, this, std::placeholders::_1)); } HostInterface::~HostInterface() @@ -173,7 +169,7 @@ void HostInterface::Connect(brls::View * view) } // ignore state for remote hosts - if(this->host->IsDiscovered() && this->host->IsReady()) + if(this->host->IsDiscovered() && !this->host->IsReady()) { // host in standby mode DIALOG(ptoyp, "Please turn on your PlayStation"); @@ -198,6 +194,11 @@ void HostInterface::ConnectSession() // user inputs are restored with the CloseStream brls::Application::blockInputs(); + // push opengl chiaki stream + // when the host is connected + this->io->SetEventConnectedCallback(std::bind(&HostInterface::Stream, this)); + this->io->SetEventQuitCallback(std::bind(&HostInterface::CloseStream, this, std::placeholders::_1)); + // connect host sesssion this->host->InitSession(this->io); this->host->StartSession(); @@ -596,7 +597,7 @@ PSRemotePlay::PSRemotePlay(IO * io, Host * host) void PSRemotePlay::draw(NVGcontext* vg, int x, int y, unsigned width, unsigned height, brls::Style* style, brls::FrameContext* ctx) { this->io->MainLoop(&this->state); - this->host->SendFeedbackState(&state); + this->host->SendFeedbackState(&this->state); // FPS calculation // this->frame_counter += 1; diff --git a/switch/src/host.cpp b/switch/src/host.cpp index 204a967..4dae777 100644 --- a/switch/src/host.cpp +++ b/switch/src/host.cpp @@ -148,8 +148,6 @@ int Host::InitSession(IO * user) memcpy(chiaki_connect_info.regist_key, this->rp_regist_key, sizeof(chiaki_connect_info.regist_key)); memcpy(chiaki_connect_info.morning, this->rp_key, sizeof(chiaki_connect_info.morning)); - // set keybord state to 0 - memset(&(this->keyboard_state), 0, sizeof(keyboard_state)); ChiakiErrorCode err = chiaki_session_init(&(this->session), &chiaki_connect_info, this->log); if(err != CHIAKI_ERR_SUCCESS) diff --git a/switch/src/io.cpp b/switch/src/io.cpp index bcbfd76..97c433a 100644 --- a/switch/src/io.cpp +++ b/switch/src/io.cpp @@ -439,6 +439,8 @@ bool IO::ReadGameKeys(SDL_Event *event, ChiakiControllerState *state) switch(event->type) { case SDL_JOYAXISMOTION: + // printf("SDL_JOYAXISMOTION jaxis %d axis %d value %d\n", + // event->jaxis.which, event->jaxis.axis, event->jaxis.value); if(event->jaxis.which == 0) { // left joystick @@ -474,7 +476,7 @@ bool IO::ReadGameKeys(SDL_Event *event, ChiakiControllerState *state) break; case SDL_JOYBUTTONDOWN: // printf("Joystick %d button %d DOWN\n", - // event->jbutton.which, event->jbutton.button); + // event->jbutton.which, event->jbutton.button); switch(event->jbutton.button) { case 0: state->buttons |= CHIAKI_CONTROLLER_BUTTON_MOON; break; // KEY_A @@ -501,7 +503,7 @@ bool IO::ReadGameKeys(SDL_Event *event, ChiakiControllerState *state) break; case SDL_JOYBUTTONUP: // printf("Joystick %d button %d UP\n", - // event->jbutton.which, event->jbutton.button); + // event->jbutton.which, event->jbutton.button); switch(event->jbutton.button) { case 0: state->buttons ^= CHIAKI_CONTROLLER_BUTTON_MOON; break; // KEY_A diff --git a/switch/src/main.cpp b/switch/src/main.cpp index 562f362..bdba8a8 100644 --- a/switch/src/main.cpp +++ b/switch/src/main.cpp @@ -56,7 +56,7 @@ static void initNxLink() s_nxlinkSock = nxlinkStdio(); if(s_nxlinkSock >= 0) - printf("initNxLink"); + printf("initNxLink\n"); else socketExit(); } @@ -83,6 +83,12 @@ extern "C" void userAppInit() // load socket custom config socketInitialize(&g_chiakiSocketInitConfig); setsysInitialize(); + + // padConfigureInput(1, HidNpadStyleSet_NpadStandard); + // PadState pad; + // padInitializeDefault(&pad); + + //hidInitializeTouchScreen(); } extern "C" void userAppExit() diff --git a/switch/src/settings.cpp b/switch/src/settings.cpp index 3488f6b..b1878ca 100644 --- a/switch/src/settings.cpp +++ b/switch/src/settings.cpp @@ -8,7 +8,7 @@ Settings::Settings() { #if defined(__SWITCH__) && !defined(CHIAKI_ENABLE_SWITCH_NXLINK) // null log for switch version - chiaki_log_init(&this->log, 0, chiaki_log_cb_print, NULL); + chiaki_log_init(&this->log, CHIAKI_LOG_ALL ^ CHIAKI_LOG_VERBOSE, chiaki_log_cb_print, NULL); #else chiaki_log_init(&this->log, CHIAKI_LOG_ALL ^ CHIAKI_LOG_VERBOSE, chiaki_log_cb_print, NULL); #endif From d31fb46ae82f179a0153e2a5de8f13897ede7112 Mon Sep 17 00:00:00 2001 From: h0neybadger Date: Sun, 27 Dec 2020 15:57:19 +0100 Subject: [PATCH 115/237] Refactor IO class as singleton --- switch/include/gui.h | 57 ++++---- switch/include/host.h | 23 +-- switch/include/io.h | 68 ++++----- switch/src/gui.cpp | 205 +++++++++++++-------------- switch/src/host.cpp | 174 ++++++++++++++--------- switch/src/io.cpp | 304 ++++++++++++++++++++++++---------------- switch/src/main.cpp | 3 +- switch/src/settings.cpp | 7 +- 8 files changed, 462 insertions(+), 379 deletions(-) diff --git a/switch/include/gui.h b/switch/include/gui.h index 8af07ca..afd2bab 100644 --- a/switch/include/gui.h +++ b/switch/include/gui.h @@ -11,55 +11,55 @@ #include -#include #include +#include -#include "host.h" -#include "settings.h" #include "discoverymanager.h" +#include "host.h" #include "io.h" +#include "settings.h" class HostInterface : public brls::List { private: - IO * io; - Host * host; - Settings * settings; + IO *io; + Host *host; + Settings *settings; bool connected = false; + public: - HostInterface(IO * io, Host * host, Settings * settings); + HostInterface(Host *host); ~HostInterface(); - static void Register(IO * io, Host * host, - Settings * settings, std::function success_cb = nullptr); + static void Register(Host *host, std::function success_cb = nullptr); void Register(); - void Wakeup(brls::View * view); - void Connect(brls::View * view); + void Wakeup(brls::View *view); + void Connect(brls::View *view); void ConnectSession(); void Disconnect(); - bool Stream(); - bool CloseStream(ChiakiQuitEvent * quit); + void Stream(); + void CloseStream(ChiakiQuitEvent *quit); }; class MainApplication { private: - Settings * settings; - ChiakiLog * log; - DiscoveryManager * discoverymanager; - IO * io; - brls::TabFrame * rootFrame; + Settings *settings; + ChiakiLog *log; + DiscoveryManager *discoverymanager; + IO *io; + brls::TabFrame *rootFrame; std::map host_menuitems; // add_host local settings std::string remote_display_name = ""; std::string remote_addr = ""; ChiakiTarget remote_ps_version = CHIAKI_TARGET_PS5_1; - bool BuildConfigurationMenu(brls::List *, Host * host = nullptr); + bool BuildConfigurationMenu(brls::List *, Host *host = nullptr); void BuildAddHostConfigurationMenu(brls::List *); - public: - MainApplication(DiscoveryManager * discoverymanager, IO * io); + public: + MainApplication(DiscoveryManager *discoverymanager); ~MainApplication(); bool Load(); }; @@ -67,24 +67,23 @@ class MainApplication class PSRemotePlay : public brls::View { private: - brls::AppletFrame * frame; + brls::AppletFrame *frame; // to display stream on screen - IO * io; + IO *io; // to send gamepad inputs - Host * host; - brls::Label * label; - ChiakiControllerState state = { 0 }; + Host *host; + brls::Label *label; + ChiakiControllerState state = {0}; // FPS calculation // double base_time; // int frame_counter = 0; // int fps = 0; public: - PSRemotePlay(IO * io, Host * host); + PSRemotePlay(Host *host); ~PSRemotePlay(); - void draw(NVGcontext * vg, int x, int y, unsigned width, unsigned height, brls::Style * style, brls::FrameContext * ctx) override; + void draw(NVGcontext *vg, int x, int y, unsigned width, unsigned height, brls::Style *style, brls::FrameContext *ctx) override; }; #endif // CHIAKI_GUI_H - diff --git a/switch/include/host.h b/switch/include/host.h index 1966c1e..ae6f05e 100644 --- a/switch/include/host.h +++ b/switch/include/host.h @@ -19,11 +19,11 @@ class DiscoveryManager; static void Discovery(ChiakiDiscoveryHost *, void *); -static void InitAudioCB(unsigned int channels, unsigned int rate, void * user); -static bool VideoCB(uint8_t * buf, size_t buf_size, void * user); -static void AudioCB(int16_t * buf, size_t samples_count, void * user); -static void RegistEventCB(ChiakiRegistEvent * event, void * user); -static void EventCB(ChiakiEvent * event, void * user); +static void InitAudioCB(unsigned int channels, unsigned int rate, void *user); +static bool VideoCB(uint8_t *buf, size_t buf_size, void *user); +static void AudioCB(int16_t *buf, size_t samples_count, void *user); +static void EventCB(ChiakiEvent *event, void *user); +static void RegistEventCB(ChiakiRegistEvent *event, void *user); enum RegistError { @@ -37,8 +37,8 @@ class Settings; class Host { private: - ChiakiLog * log = nullptr; - Settings * settings = nullptr; + ChiakiLog *log = nullptr; + Settings *settings = nullptr; //video config ChiakiVideoResolutionPreset video_resolution = CHIAKI_VIDEO_RESOLUTION_PRESET_720p; ChiakiVideoFPSPreset video_fps = CHIAKI_VIDEO_FPS_PRESET_60; @@ -52,6 +52,9 @@ class Host std::function chiaki_regist_event_type_finished_canceled = nullptr; std::function chiaki_regist_event_type_finished_failed = nullptr; std::function chiaki_regist_event_type_finished_success = nullptr; + std::function chiaki_event_connected_cb = nullptr; + std::function chiaki_even_login_pin_request_cb = nullptr; + std::function chiaki_event_quit_cb = nullptr; // internal state bool discovered = false; @@ -95,7 +98,8 @@ class Host void StartSession(); void SendFeedbackState(ChiakiControllerState *); void RegistCB(ChiakiRegistEvent *); - bool GetVideoResolution(int * ret_width, int * ret_height); + void ConnectionEventCB(ChiakiEvent *); + bool GetVideoResolution(int *ret_width, int *ret_height); std::string GetHostName(); std::string GetHostAddr(); ChiakiTarget GetChiakiTarget(); @@ -104,6 +108,9 @@ class Host void SetRegistEventTypeFinishedCanceled(std::function chiaki_regist_event_type_finished_canceled); void SetRegistEventTypeFinishedFailed(std::function chiaki_regist_event_type_finished_failed); void SetRegistEventTypeFinishedSuccess(std::function chiaki_regist_event_type_finished_success); + void SetEventConnectedCallback(std::function chiaki_event_connected_cb); + void SetEventLoginPinRequestCallback(std::function chiaki_even_login_pin_request_cb); + void SetEventQuitCallback(std::function chiaki_event_quit_cb); bool IsRegistered(); bool IsDiscovered(); bool IsReady(); diff --git a/switch/include/io.h b/switch/include/io.h index 932a2b3..8427684 100644 --- a/switch/include/io.h +++ b/switch/include/io.h @@ -3,15 +3,14 @@ #ifndef CHIAKI_IO_H #define CHIAKI_IO_H -#include -#include #include +#include +#include #include // glad library (OpenGL loader) #include - /* https://github.com/devkitPro/switch-glad/blob/master/include/glad/glad.h https://glad.dav1d.de/#profile=core&language=c&specification=gl&api=gl%3D4.3&extensions=GL_EXT_texture_compression_s3tc&extensions=GL_EXT_texture_filter_anisotropic @@ -37,8 +36,8 @@ extern "C" #include } -#include #include +#include #include "exception.h" @@ -47,8 +46,11 @@ extern "C" class IO { + protected: + IO(); + static IO * instance; private: - ChiakiLog * log; + ChiakiLog *log; int video_width; int video_height; bool quit = false; @@ -57,15 +59,12 @@ class IO // default nintendo switch res int screen_width = 1280; int screen_height = 720; - std::function chiaki_event_connected_cb = nullptr; - std::function chiaki_even_login_pin_request_cb = nullptr; - std::function chiaki_event_quit_cb = nullptr; - AVCodec * codec; - AVCodecContext * codec_context; - AVFrame * frame; + AVCodec *codec; + AVCodecContext *codec_context; + AVFrame *frame; SDL_AudioDeviceID sdl_audio_device_id = 0; SDL_Event sdl_event; - SDL_Joystick * sdl_joystick_ptr[SDL_JOYSTICK_COUNT] = {0}; + SDL_Joystick *sdl_joystick_ptr[SDL_JOYSTICK_COUNT] = {0}; GLuint vao; GLuint vbo; GLuint tex[PLANES_COUNT]; @@ -73,49 +72,38 @@ class IO GLuint vert; GLuint frag; GLuint prog; - private: bool InitAVCodec(); bool InitOpenGl(); bool InitOpenGlTextures(); bool InitOpenGlShader(); void OpenGlDraw(); #ifdef DEBUG_OPENGL - void CheckGLError(const char * func, const char * file, int line); - void DumpShaderError(GLuint prog, const char * func, const char * file, int line); - void DumpProgramError(GLuint prog, const char * func, const char * file, int line); + void CheckGLError(const char *func, const char *file, int line); + void DumpShaderError(GLuint prog, const char *func, const char *file, int line); + void DumpProgramError(GLuint prog, const char *func, const char *file, int line); #endif - GLuint CreateAndCompileShader(GLenum type, const char * source); - void SetOpenGlYUVPixels(AVFrame * frame); - bool ReadGameKeys(SDL_Event * event, ChiakiControllerState * state); - bool ReadGameTouchScreen(ChiakiControllerState * state); + GLuint CreateAndCompileShader(GLenum type, const char *source); + void SetOpenGlYUVPixels(AVFrame *frame); + bool ReadGameKeys(SDL_Event *event, ChiakiControllerState *state); + bool ReadGameTouchScreen(ChiakiControllerState *state); + public: - IO(ChiakiLog * log); + // singleton configuration + IO(const IO&) = delete; + void operator=(const IO&) = delete; + static IO * GetInstance(); + ~IO(); void SetMesaConfig(); - bool VideoCB(uint8_t * buf, size_t buf_size); - void SetEventConnectedCallback(std::function chiaki_event_connected_cb) - { - this->chiaki_event_connected_cb = chiaki_event_connected_cb; - }; - void SetEventLoginPinRequestCallback(std::function chiaki_even_login_pin_request_cb) - { - this->chiaki_even_login_pin_request_cb = chiaki_even_login_pin_request_cb; - }; - void SetEventQuitCallback(std::function chiaki_event_quit_cb) - { - this->chiaki_event_quit_cb = chiaki_event_quit_cb; - }; + bool VideoCB(uint8_t *buf, size_t buf_size); void InitAudioCB(unsigned int channels, unsigned int rate); - void AudioCB(int16_t * buf, size_t samples_count); - void EventCB(ChiakiEvent *event); + void AudioCB(int16_t *buf, size_t samples_count); bool InitVideo(int video_width, int video_height, int screen_width, int screen_height); bool FreeVideo(); bool InitJoystick(); bool FreeJoystick(); - bool ReadUserKeyboard(char * buffer, size_t buffer_size); - bool MainLoop(ChiakiControllerState * state); + bool ReadUserKeyboard(char *buffer, size_t buffer_size); + bool MainLoop(ChiakiControllerState *state); }; #endif //CHIAKI_IO_H - - diff --git a/switch/src/gui.cpp b/switch/src/gui.cpp index 3755cfe..6d4ef2a 100644 --- a/switch/src/gui.cpp +++ b/switch/src/gui.cpp @@ -1,7 +1,7 @@ // SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL -#include #include "gui.h" +#include #define SCREEN_W 1280 #define SCREEN_H 720 @@ -9,34 +9,39 @@ // TODO using namespace brls::i18n::literals; // for _i18n -#define DIALOG(dialog, r) \ - brls::Dialog* d_##dialog = new brls::Dialog(r); \ - brls::GenericEvent::Callback cb_##dialog = [d_##dialog](brls::View* view) \ - { \ - d_##dialog->close(); \ - }; \ - d_##dialog->addButton("Ok", cb_##dialog); \ - d_##dialog->setCancelable(false); \ - d_##dialog->open(); \ +#define DIALOG(dialog, r) \ + brls::Dialog *d_##dialog = new brls::Dialog(r); \ + brls::GenericEvent::Callback cb_##dialog = [d_##dialog](brls::View *view) { \ + d_##dialog->close(); \ + }; \ + d_##dialog->addButton("Ok", cb_##dialog); \ + d_##dialog->setCancelable(false); \ + d_##dialog->open(); \ brls::Logger::info("Dialog: {0}", r); - -HostInterface::HostInterface(IO * io, Host * host, Settings * settings) - : io(io), host(host), settings(settings) +HostInterface::HostInterface(Host *host) + : host(host) { - brls::ListItem* connect = new brls::ListItem("Connect"); + this->settings = Settings::GetInstance(); + this->io = IO::GetInstance(); + + brls::ListItem *connect = new brls::ListItem("Connect"); connect->getClickEvent()->subscribe(std::bind(&HostInterface::Connect, this, std::placeholders::_1)); this->addView(connect); - brls::ListItem* wakeup = new brls::ListItem("Wakeup"); + brls::ListItem *wakeup = new brls::ListItem("Wakeup"); wakeup->getClickEvent()->subscribe(std::bind(&HostInterface::Wakeup, this, std::placeholders::_1)); this->addView(wakeup); // message delimiter - brls::Label* info = new brls::Label(brls::LabelStyle::REGULAR, - "Host configuration", true); + brls::Label *info = new brls::Label(brls::LabelStyle::REGULAR, + "Host configuration", true); this->addView(info); + // push opengl chiaki stream + // when the host is connected + this->host->SetEventConnectedCallback(std::bind(&HostInterface::Stream, this)); + this->host->SetEventQuitCallback(std::bind(&HostInterface::CloseStream, this, std::placeholders::_1)); } HostInterface::~HostInterface() @@ -44,8 +49,11 @@ HostInterface::~HostInterface() Disconnect(); } -void HostInterface::Register(IO * io, Host * host, Settings * settings, std::function success_cb) +void HostInterface::Register(Host *host, std::function success_cb) { + Settings *settings = Settings::GetInstance(); + IO *io = IO::GetInstance(); + // user must provide psn id for registration std::string account_id = settings->GetPSNAccountID(host); std::string online_id = settings->GetPSNOnlineID(host); @@ -65,35 +73,32 @@ void HostInterface::Register(IO * io, Host * host, Settings * settings, std::fun } // add HostConnected function to regist_event_type_finished_success - auto event_type_finished_success_cb = [settings, success_cb]() - { - // save RP keys - settings->WriteFile(); - if(success_cb != nullptr) - { - // FIXME: may raise a connection refused - // when the connection is triggered - // just after the register success - sleep(2); - success_cb(); - } - // decrement block input token number - brls::Application::unblockInputs(); + auto event_type_finished_success_cb = [settings, success_cb]() { + // save RP keys + settings->WriteFile(); + if(success_cb != nullptr) + { + // FIXME: may raise a connection refused + // when the connection is triggered + // just after the register success + sleep(2); + success_cb(); + } + // decrement block input token number + brls::Application::unblockInputs(); }; host->SetRegistEventTypeFinishedSuccess(event_type_finished_success_cb); - auto event_type_finished_failed_cb = []() - { - // unlock user inputs - brls::Application::unblockInputs(); - brls::Application::notify("Registration failed"); + auto event_type_finished_failed_cb = []() { + // unlock user inputs + brls::Application::unblockInputs(); + brls::Application::notify("Registration failed"); }; host->SetRegistEventTypeFinishedFailed(event_type_finished_failed_cb); // the host is not registered yet - brls::Dialog* peprpc = new brls::Dialog("Please enter your PlayStation registration PIN code"); - brls::GenericEvent::Callback cb_peprpc = [host, io, peprpc](brls::View* view) - { + brls::Dialog *peprpc = new brls::Dialog("Please enter your PlayStation registration PIN code"); + brls::GenericEvent::Callback cb_peprpc = [host, io, peprpc](brls::View *view) { bool pin_provided = false; char pin_input[9] = {0}; std::string error_message; @@ -133,11 +138,10 @@ void HostInterface::Register(IO * io, Host * host, Settings * settings, std::fun void HostInterface::Register() { // use Connect just after the registration to save user inputs - HostInterface::Register(this->io, this->host, - this->settings, std::bind(&HostInterface::ConnectSession, this)); + HostInterface::Register(this->host, std::bind(&HostInterface::ConnectSession, this)); } -void HostInterface::Wakeup(brls::View * view) +void HostInterface::Wakeup(brls::View *view) { if(!this->host->HasRPkey()) { @@ -158,7 +162,7 @@ void HostInterface::Wakeup(brls::View * view) } } -void HostInterface::Connect(brls::View * view) +void HostInterface::Connect(brls::View *view) { // check that all requirements are met if(!this->host->IsDiscovered() && !this->host->HasRPkey()) @@ -194,11 +198,6 @@ void HostInterface::ConnectSession() // user inputs are restored with the CloseStream brls::Application::blockInputs(); - // push opengl chiaki stream - // when the host is connected - this->io->SetEventConnectedCallback(std::bind(&HostInterface::Stream, this)); - this->io->SetEventQuitCallback(std::bind(&HostInterface::CloseStream, this, std::placeholders::_1)); - // connect host sesssion this->host->InitSession(this->io); this->host->StartSession(); @@ -212,10 +211,11 @@ void HostInterface::Disconnect() this->host->StopSession(); this->connected = false; } + this->host->FiniSession(); } -bool HostInterface::Stream() +void HostInterface::Stream() { this->connected = true; // https://github.com/natinusala/borealis/issues/59 @@ -226,11 +226,10 @@ bool HostInterface::Stream() // brls::Application::setDisplayFramerate(true); // push raw opengl stream over borealis - brls::Application::pushView(new PSRemotePlay(this->io, this->host)); - return true; + brls::Application::pushView(new PSRemotePlay(this->host)); } -bool HostInterface::CloseStream(ChiakiQuitEvent * quit) +void HostInterface::CloseStream(ChiakiQuitEvent *quit) { // session QUIT call back brls::Application::unblockInputs(); @@ -244,14 +243,14 @@ bool HostInterface::CloseStream(ChiakiQuitEvent * quit) */ brls::Application::notify(chiaki_quit_reason_string(quit->reason)); Disconnect(); - return false; } -MainApplication::MainApplication(DiscoveryManager * discoverymanager, IO * io) - : discoverymanager(discoverymanager), io(io) +MainApplication::MainApplication(DiscoveryManager *discoverymanager) + : discoverymanager(discoverymanager) { this->settings = Settings::GetInstance(); this->log = this->settings->GetLogger(); + this->io = IO::GetInstance(); } MainApplication::~MainApplication() @@ -268,7 +267,7 @@ bool MainApplication::Load() brls::Logger::setLogLevel(brls::LogLevel::DEBUG); brls::i18n::loadTranslations(); - if (!brls::Application::init("Chiaki Remote play")) + if(!brls::Application::init("Chiaki Remote play")) { brls::Logger::error("Unable to init Borealis application"); return false; @@ -293,8 +292,8 @@ bool MainApplication::Load() this->rootFrame->setTitle("Chiaki: Open Source PlayStation Remote Play Client"); this->rootFrame->setIcon(BOREALIS_ASSET("icon.jpg")); - brls::List* config = new brls::List(); - brls::List* add_host = new brls::List(); + brls::List *config = new brls::List(); + brls::List *add_host = new brls::List(); BuildConfigurationMenu(config); BuildAddHostConfigurationMenu(add_host); @@ -306,16 +305,15 @@ bool MainApplication::Load() // Add the root view to the stack brls::Application::pushView(this->rootFrame); - std::map * hosts = this->settings->GetHostsMap(); + std::map *hosts = this->settings->GetHostsMap(); while(brls::Application::mainLoop()) { for(auto it = hosts->begin(); it != hosts->end(); it++) { // add host to the gui only if the host is registered or discovered - if(this->host_menuitems.find(&it->second) == this->host_menuitems.end() - && (it->second.HasRPkey() == true || it->second.IsDiscovered() == true)) + if(this->host_menuitems.find(&it->second) == this->host_menuitems.end() && (it->second.HasRPkey() == true || it->second.IsDiscovered() == true)) { - HostInterface * new_host = new HostInterface(this->io, &it->second, this->settings); + HostInterface *new_host = new HostInterface(&it->second); this->host_menuitems[&it->second] = new_host; // create host if udefined BuildConfigurationMenu(new_host, &it->second); @@ -326,13 +324,12 @@ bool MainApplication::Load() return true; } -bool MainApplication::BuildConfigurationMenu(brls::List * ls, Host * host) +bool MainApplication::BuildConfigurationMenu(brls::List *ls, Host *host) { std::string psn_account_id_string = this->settings->GetPSNAccountID(host); - brls::ListItem* psn_account_id = new brls::ListItem("PSN Account ID", "PS5 or PS4 v7.0 and greater (base64 account_id)"); + brls::ListItem *psn_account_id = new brls::ListItem("PSN Account ID", "PS5 or PS4 v7.0 and greater (base64 account_id)"); psn_account_id->setValue(psn_account_id_string.c_str()); - auto psn_account_id_cb = [this, host, psn_account_id](brls::View * view) - { + auto psn_account_id_cb = [this, host, psn_account_id](brls::View *view) { char account_id[CHIAKI_PSN_ACCOUNT_ID_SIZE * 2] = {0}; bool input = this->io->ReadUserKeyboard(account_id, sizeof(account_id)); if(input) @@ -349,10 +346,9 @@ bool MainApplication::BuildConfigurationMenu(brls::List * ls, Host * host) ls->addView(psn_account_id); std::string psn_online_id_string = this->settings->GetPSNOnlineID(host); - brls::ListItem* psn_online_id = new brls::ListItem("PSN Online ID"); + brls::ListItem *psn_online_id = new brls::ListItem("PSN Online ID"); psn_online_id->setValue(psn_online_id_string.c_str()); - auto psn_online_id_cb = [this, host, psn_online_id](brls::View * view) - { + auto psn_online_id_cb = [this, host, psn_online_id](brls::View *view) { char online_id[256] = {0}; bool input = this->io->ReadUserKeyboard(online_id, sizeof(online_id)); if(input) @@ -383,11 +379,10 @@ bool MainApplication::BuildConfigurationMenu(brls::List * ls, Host * host) break; } - brls::SelectListItem* resolution = new brls::SelectListItem( - "Resolution", { "720p", "540p", "360p" }, value); + brls::SelectListItem *resolution = new brls::SelectListItem( + "Resolution", {"720p", "540p", "360p"}, value); - auto resolution_cb = [this, host](int result) - { + auto resolution_cb = [this, host](int result) { ChiakiVideoResolutionPreset value = CHIAKI_VIDEO_RESOLUTION_PRESET_720p; switch(result) { @@ -418,11 +413,10 @@ bool MainApplication::BuildConfigurationMenu(brls::List * ls, Host * host) break; } - brls::SelectListItem* fps = new brls::SelectListItem( - "FPS", { "60", "30"}, value); + brls::SelectListItem *fps = new brls::SelectListItem( + "FPS", {"60", "30"}, value); - auto fps_cb = [this, host](int result) - { + auto fps_cb = [this, host](int result) { ChiakiVideoFPSPreset value = CHIAKI_VIDEO_FPS_PRESET_60; switch(result) { @@ -443,49 +437,47 @@ bool MainApplication::BuildConfigurationMenu(brls::List * ls, Host * host) if(host != nullptr) { // message delimiter - brls::Label* info = new brls::Label(brls::LabelStyle::REGULAR, - "Host information", true); + brls::Label *info = new brls::Label(brls::LabelStyle::REGULAR, + "Host information", true); ls->addView(info); std::string host_name_string = this->settings->GetHostName(host); - brls::ListItem* host_name = new brls::ListItem("PS Hostname"); + brls::ListItem *host_name = new brls::ListItem("PS Hostname"); host_name->setValue(host_name_string.c_str()); ls->addView(host_name); std::string host_addr_string = settings->GetHostAddr(host); - brls::ListItem* host_addr = new brls::ListItem("PS4 Address"); + brls::ListItem *host_addr = new brls::ListItem("PS4 Address"); host_addr->setValue(host_addr_string.c_str()); ls->addView(host_addr); std::string host_rp_regist_key_string = settings->GetHostRPRegistKey(host); - brls::ListItem* host_rp_regist_key = new brls::ListItem("RP Register Key"); + brls::ListItem *host_rp_regist_key = new brls::ListItem("RP Register Key"); host_rp_regist_key->setValue(host_rp_regist_key_string.c_str()); ls->addView(host_rp_regist_key); std::string host_rp_key_string = settings->GetHostRPKey(host); - brls::ListItem* host_rp_key = new brls::ListItem("RP Key"); + brls::ListItem *host_rp_key = new brls::ListItem("RP Key"); host_rp_key->setValue(host_rp_key_string.c_str()); ls->addView(host_rp_key); std::string host_rp_key_type_string = std::to_string(settings->GetHostRPKeyType(host)); - brls::ListItem* host_rp_key_type = new brls::ListItem("RP Key type"); + brls::ListItem *host_rp_key_type = new brls::ListItem("RP Key type"); host_rp_key_type->setValue(host_rp_key_type_string.c_str()); ls->addView(host_rp_key_type); - } return true; } -void MainApplication::BuildAddHostConfigurationMenu(brls::List * add_host) +void MainApplication::BuildAddHostConfigurationMenu(brls::List *add_host) { // create host for wan connection // brls::Label* add_host_label = new brls::Label(brls::LabelStyle::REGULAR, // "Add Host configuration", true); - brls::ListItem* display_name = new brls::ListItem("Display name"); - auto display_name_cb = [this, display_name](brls::View * view) - { + brls::ListItem *display_name = new brls::ListItem("Display name"); + auto display_name_cb = [this, display_name](brls::View *view) { char name[16] = {0}; bool input = this->io->ReadUserKeyboard(name, sizeof(name)); if(input) @@ -499,9 +491,8 @@ void MainApplication::BuildAddHostConfigurationMenu(brls::List * add_host) display_name->getClickEvent()->subscribe(display_name_cb); add_host->addView(display_name); - brls::ListItem* address = new brls::ListItem("Remote IP/name"); - auto address_cb = [this, address](brls::View * view) - { + brls::ListItem *address = new brls::ListItem("Remote IP/name"); + auto address_cb = [this, address](brls::View *view) { char addr[256] = {0}; bool input = this->io->ReadUserKeyboard(addr, sizeof(addr)); if(input) @@ -515,15 +506,13 @@ void MainApplication::BuildAddHostConfigurationMenu(brls::List * add_host) address->getClickEvent()->subscribe(address_cb); add_host->addView(address); - // TODO // brls::ListItem* port = new brls::ListItem("Remote session port", "tcp/udp 9295"); // brls::ListItem* port = new brls::ListItem("Remote stream port", "udp 9296"); // brls::ListItem* port = new brls::ListItem("Remote Senkusha port", "udp 9297"); - brls::SelectListItem* ps_version = new brls::SelectListItem("PlayStation Version", - { "PS5", "PS4 > 8", "7 < PS4 < 8", "PS4 < 7"}); - auto ps_version_cb = [this, ps_version](int result) - { + brls::SelectListItem *ps_version = new brls::SelectListItem("PlayStation Version", + {"PS5", "PS4 > 8", "7 < PS4 < 8", "PS4 < 7"}); + auto ps_version_cb = [this, ps_version](int result) { switch(result) { case 0: @@ -547,9 +536,8 @@ void MainApplication::BuildAddHostConfigurationMenu(brls::List * add_host) ps_version->getValueSelectedEvent()->subscribe(ps_version_cb); add_host->addView(ps_version); - brls::ListItem* register_host = new brls::ListItem("Register"); - auto register_host_cb = [this](brls::View * view) - { + brls::ListItem *register_host = new brls::ListItem("Register"); + auto register_host_cb = [this](brls::View *view) { bool err = false; if(this->remote_display_name.length() <= 0) { @@ -572,29 +560,31 @@ void MainApplication::BuildAddHostConfigurationMenu(brls::List * add_host) if(err) return; - Host * host = this->settings->GetOrCreateHost(&this->remote_display_name); + Host *host = this->settings->GetOrCreateHost(&this->remote_display_name); host->SetHostAddr(this->remote_addr); host->SetChiakiTarget(this->remote_ps_version); - HostInterface::Register(this->io, host, this->settings); + HostInterface::Register(host); }; register_host->getClickEvent()->subscribe(register_host_cb); add_host->addView(register_host); } -PSRemotePlay::PSRemotePlay(IO * io, Host * host) - : io(io), host(host) +PSRemotePlay::PSRemotePlay(Host *host) + : host(host) { + this->io = IO::GetInstance(); + // store joycon/touchpad keys - for(int x=0; x < CHIAKI_CONTROLLER_TOUCHES_MAX; x++) + for(int x = 0; x < CHIAKI_CONTROLLER_TOUCHES_MAX; x++) // start touchpad as "untouched" this->state.touches[x].id = -1; // this->base_time=glfwGetTime(); } -void PSRemotePlay::draw(NVGcontext* vg, int x, int y, unsigned width, unsigned height, brls::Style* style, brls::FrameContext* ctx) +void PSRemotePlay::draw(NVGcontext *vg, int x, int y, unsigned width, unsigned height, brls::Style *style, brls::FrameContext *ctx) { this->io->MainLoop(&this->state); this->host->SendFeedbackState(&this->state); @@ -622,4 +612,3 @@ void PSRemotePlay::draw(NVGcontext* vg, int x, int y, unsigned width, unsigned h PSRemotePlay::~PSRemotePlay() { } - diff --git a/switch/src/host.cpp b/switch/src/host.cpp index 4dae777..0770ca0 100644 --- a/switch/src/host.cpp +++ b/switch/src/host.cpp @@ -7,33 +7,33 @@ #include "host.h" #include "io.h" -static void InitAudioCB(unsigned int channels, unsigned int rate, void * user) +static void InitAudioCB(unsigned int channels, unsigned int rate, void *user) { - IO * io = (IO *)user; + IO *io = (IO *)user; io->InitAudioCB(channels, rate); } -static bool VideoCB(uint8_t * buf, size_t buf_size, void * user) +static bool VideoCB(uint8_t *buf, size_t buf_size, void *user) { - IO * io = (IO *)user; + IO *io = (IO *)user; return io->VideoCB(buf, buf_size); } -static void AudioCB(int16_t * buf, size_t samples_count, void * user) +static void AudioCB(int16_t *buf, size_t samples_count, void *user) { - IO * io = (IO *)user; + IO *io = (IO *)user; io->AudioCB(buf, samples_count); } -static void EventCB(ChiakiEvent * event, void * user) +static void EventCB(ChiakiEvent *event, void *user) { - IO * io = (IO *)user; - io->EventCB(event); + Host *host = (Host *)user; + host->ConnectionEventCB(event); } -static void RegistEventCB(ChiakiRegistEvent * event, void * user) +static void RegistEventCB(ChiakiRegistEvent *event, void *user) { - Host * host = (Host *)user; + Host *host = (Host *)user; host->RegistCB(event); } @@ -132,7 +132,7 @@ int Host::Register(std::string pin) return HOST_REGISTER_OK; } -int Host::InitSession(IO * user) +int Host::InitSession(IO *user) { chiaki_connect_video_profile_preset(&(this->video_profile), this->video_resolution, this->video_fps); @@ -158,7 +158,7 @@ int Host::InitSession(IO * user) chiaki_opus_decoder_get_sink(&this->opus_decoder, &audio_sink); chiaki_session_set_audio_sink(&(this->session), &audio_sink); chiaki_session_set_video_sample_cb(&(this->session), VideoCB, user); - chiaki_session_set_event_cb(&(this->session), EventCB, user); + chiaki_session_set_event_cb(&(this->session), EventCB, this); return 0; } @@ -166,6 +166,7 @@ int Host::FiniSession() { if(this->session_init) { + this->session_init = false; chiaki_session_join(&this->session); chiaki_session_fini(&this->session); chiaki_opus_decoder_fini(&this->opus_decoder); @@ -188,13 +189,35 @@ void Host::StartSession() } } -void Host::SendFeedbackState(ChiakiControllerState * state) +void Host::SendFeedbackState(ChiakiControllerState *state) { // send controller/joystick key chiaki_session_set_controller_state(&this->session, state); } -void Host::RegistCB(ChiakiRegistEvent * event) +void Host::ConnectionEventCB(ChiakiEvent *event) +{ + switch(event->type) + { + case CHIAKI_EVENT_CONNECTED: + CHIAKI_LOGI(this->log, "EventCB CHIAKI_EVENT_CONNECTED"); + if(this->chiaki_event_connected_cb != nullptr) + this->chiaki_event_connected_cb(); + break; + case CHIAKI_EVENT_LOGIN_PIN_REQUEST: + CHIAKI_LOGI(this->log, "EventCB CHIAKI_EVENT_LOGIN_PIN_REQUEST"); + if(this->chiaki_even_login_pin_request_cb != nullptr) + this->chiaki_even_login_pin_request_cb(event->login_pin_request.pin_incorrect); + break; + case CHIAKI_EVENT_QUIT: + CHIAKI_LOGI(this->log, "EventCB CHIAKI_EVENT_QUIT"); + if(this->chiaki_event_quit_cb != nullptr) + this->chiaki_event_quit_cb(&event->quit); + break; + } +} + +void Host::RegistCB(ChiakiRegistEvent *event) { // Chiaki callback fuction // fuction called by lib chiaki regist @@ -205,71 +228,71 @@ void Host::RegistCB(ChiakiRegistEvent * event) this->registered = false; switch(event->type) { - case CHIAKI_REGIST_EVENT_TYPE_FINISHED_CANCELED: - CHIAKI_LOGI(this->log, "Register event CHIAKI_REGIST_EVENT_TYPE_FINISHED_CANCELED"); - if(this->chiaki_regist_event_type_finished_canceled != nullptr) + case CHIAKI_REGIST_EVENT_TYPE_FINISHED_CANCELED: + CHIAKI_LOGI(this->log, "Register event CHIAKI_REGIST_EVENT_TYPE_FINISHED_CANCELED"); + if(this->chiaki_regist_event_type_finished_canceled != nullptr) + { + this->chiaki_regist_event_type_finished_canceled(); + } + break; + case CHIAKI_REGIST_EVENT_TYPE_FINISHED_FAILED: + CHIAKI_LOGI(this->log, "Register event CHIAKI_REGIST_EVENT_TYPE_FINISHED_FAILED"); + if(this->chiaki_regist_event_type_finished_failed != nullptr) + { + this->chiaki_regist_event_type_finished_failed(); + } + break; + case CHIAKI_REGIST_EVENT_TYPE_FINISHED_SUCCESS: { - this->chiaki_regist_event_type_finished_canceled(); - } - break; - case CHIAKI_REGIST_EVENT_TYPE_FINISHED_FAILED: - CHIAKI_LOGI(this->log, "Register event CHIAKI_REGIST_EVENT_TYPE_FINISHED_FAILED"); - if(this->chiaki_regist_event_type_finished_failed != nullptr) - { - this->chiaki_regist_event_type_finished_failed(); - } - break; - case CHIAKI_REGIST_EVENT_TYPE_FINISHED_SUCCESS: - { - ChiakiRegisteredHost * r_host = event->registered_host; - CHIAKI_LOGI(this->log, "Register event CHIAKI_REGIST_EVENT_TYPE_FINISHED_SUCCESS"); - // copy values form ChiakiRegisteredHost object - this->ap_ssid = r_host->ap_ssid; - this->ap_key = r_host->ap_key; - this->ap_name = r_host->ap_name; - memcpy(&(this->server_mac), &(r_host->server_mac), sizeof(this->server_mac)); - this->server_nickname = r_host->server_nickname; - memcpy(&(this->rp_regist_key), &(r_host->rp_regist_key), sizeof(this->rp_regist_key)); - this->rp_key_type = r_host->rp_key_type; - memcpy(&(this->rp_key), &(r_host->rp_key), sizeof(this->rp_key)); - // mark host as registered - this->registered = true; - this->rp_key_data = true; - CHIAKI_LOGI(this->log, "Register Success %s", this->host_name.c_str()); + ChiakiRegisteredHost *r_host = event->registered_host; + CHIAKI_LOGI(this->log, "Register event CHIAKI_REGIST_EVENT_TYPE_FINISHED_SUCCESS"); + // copy values form ChiakiRegisteredHost object + this->ap_ssid = r_host->ap_ssid; + this->ap_key = r_host->ap_key; + this->ap_name = r_host->ap_name; + memcpy(&(this->server_mac), &(r_host->server_mac), sizeof(this->server_mac)); + this->server_nickname = r_host->server_nickname; + memcpy(&(this->rp_regist_key), &(r_host->rp_regist_key), sizeof(this->rp_regist_key)); + this->rp_key_type = r_host->rp_key_type; + memcpy(&(this->rp_key), &(r_host->rp_key), sizeof(this->rp_key)); + // mark host as registered + this->registered = true; + this->rp_key_data = true; + CHIAKI_LOGI(this->log, "Register Success %s", this->host_name.c_str()); - if(this->chiaki_regist_event_type_finished_success != nullptr) - this->chiaki_regist_event_type_finished_success(); + if(this->chiaki_regist_event_type_finished_success != nullptr) + this->chiaki_regist_event_type_finished_success(); - break; - } + break; + } } // close registration socket chiaki_regist_stop(&this->regist); chiaki_regist_fini(&this->regist); } -bool Host::GetVideoResolution(int * ret_width, int * ret_height) +bool Host::GetVideoResolution(int *ret_width, int *ret_height) { switch(this->video_resolution) { - case CHIAKI_VIDEO_RESOLUTION_PRESET_360p: - *ret_width = 640; - *ret_height = 360; - break; - case CHIAKI_VIDEO_RESOLUTION_PRESET_540p: - *ret_width = 950; - *ret_height = 540; - break; - case CHIAKI_VIDEO_RESOLUTION_PRESET_720p: - *ret_width = 1280; - *ret_height = 720; - break; - case CHIAKI_VIDEO_RESOLUTION_PRESET_1080p: - *ret_width = 1920; - *ret_height = 1080; - break; - default: - return false; + case CHIAKI_VIDEO_RESOLUTION_PRESET_360p: + *ret_width = 640; + *ret_height = 360; + break; + case CHIAKI_VIDEO_RESOLUTION_PRESET_540p: + *ret_width = 950; + *ret_height = 540; + break; + case CHIAKI_VIDEO_RESOLUTION_PRESET_720p: + *ret_width = 1280; + *ret_height = 720; + break; + case CHIAKI_VIDEO_RESOLUTION_PRESET_1080p: + *ret_width = 1920; + *ret_height = 1080; + break; + default: + return false; } return true; } @@ -314,6 +337,21 @@ void Host::SetRegistEventTypeFinishedSuccess(std::function chiaki_regist this->chiaki_regist_event_type_finished_success = chiaki_regist_event_type_finished_success; } +void Host::SetEventConnectedCallback(std::function chiaki_event_connected_cb) +{ + this->chiaki_event_connected_cb = chiaki_event_connected_cb; +} + +void Host::SetEventLoginPinRequestCallback(std::function chiaki_even_login_pin_request_cb) +{ + this->chiaki_even_login_pin_request_cb = chiaki_even_login_pin_request_cb; +} + +void Host::SetEventQuitCallback(std::function chiaki_event_quit_cb) +{ + this->chiaki_event_quit_cb = chiaki_event_quit_cb; +} + bool Host::IsRegistered() { return this->registered; diff --git a/switch/src/io.cpp b/switch/src/io.cpp index 97c433a..d7c28f7 100644 --- a/switch/src/io.cpp +++ b/switch/src/io.cpp @@ -7,6 +7,7 @@ #endif #include "io.h" +#include "settings.h" // https://github.com/torvalds/linux/blob/41ba50b0572e90ed3d24fe4def54567e9050bc47/drivers/hid/hid-sony.c#L2742 #define DS4_TRACKPAD_MAX_X 1920 @@ -24,7 +25,7 @@ // use OpenGl to decode YUV // the aim is to spare CPU load on nintendo switch -static const char* shader_vert_glsl = R"glsl( +static const char *shader_vert_glsl = R"glsl( #version 150 core in vec2 pos_attr; out vec2 uv_var; @@ -60,12 +61,23 @@ static const float vert_pos[] = { 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, - 1.0f, 1.0f -}; + 1.0f, 1.0f}; -IO::IO(ChiakiLog * log) - : log(log) +IO *IO::instance = nullptr; + +IO *IO::GetInstance() { + if(instance == nullptr) + { + instance = new IO; + } + return instance; +} + +IO::IO() +{ + Settings *settings = Settings::GetInstance(); + this->log = settings->GetLogger(); } IO::~IO() @@ -99,11 +111,15 @@ void IO::SetMesaConfig() } #ifdef DEBUG_OPENGL -#define D(x){ (x); CheckGLError(__func__, __FILE__, __LINE__); } -void IO::CheckGLError(const char* func, const char* file, int line) +#define D(x) \ + { \ + (x); \ + CheckGLError(__func__, __FILE__, __LINE__); \ + } +void IO::CheckGLError(const char *func, const char *file, int line) { GLenum err; - while( (err = glGetError()) != GL_NO_ERROR ) + while((err = glGetError()) != GL_NO_ERROR) { CHIAKI_LOGE(this->log, "glGetError: %x function: %s from %s line %d", err, func, file, line); //GL_INVALID_VALUE, 0x0501 @@ -115,37 +131,51 @@ void IO::CheckGLError(const char* func, const char* file, int line) } } -#define DS(x){ DumpShaderError(x, __func__, __FILE__, __LINE__); } -void IO::DumpShaderError(GLuint shader, const char* func, const char* file, int line) +#define DS(x) \ + { \ + DumpShaderError(x, __func__, __FILE__, __LINE__); \ + } +void IO::DumpShaderError(GLuint shader, const char *func, const char *file, int line) { - GLchar str[512+1]; + GLchar str[512 + 1]; GLsizei len = 0; glGetShaderInfoLog(shader, 512, &len, str); - if (len > 512) len = 512; + if(len > 512) + len = 512; str[len] = '\0'; CHIAKI_LOGE(this->log, "glGetShaderInfoLog: %s function: %s from %s line %d", str, func, file, line); } -#define DP(x){ DumpProgramError(x, __func__, __FILE__, __LINE__); } -void IO::DumpProgramError(GLuint prog, const char* func, const char* file, int line) +#define DP(x) \ + { \ + DumpProgramError(x, __func__, __FILE__, __LINE__); \ + } +void IO::DumpProgramError(GLuint prog, const char *func, const char *file, int line) { - GLchar str[512+1]; + GLchar str[512 + 1]; GLsizei len = 0; glGetProgramInfoLog(prog, 512, &len, str); - if (len > 512) len = 512; + if(len > 512) + len = 512; str[len] = '\0'; CHIAKI_LOGE(this->log, "glGetProgramInfoLog: %s function: %s from %s line %d", str, func, file, line); } #else // do nothing -#define D(x){ (x); } -#define DS(x){ } -#define DP(x){ } +#define D(x) \ + { \ + (x); \ + } +#define DS(x) \ + { \ + } +#define DP(x) \ + { \ + } #endif - -bool IO::VideoCB(uint8_t * buf, size_t buf_size) +bool IO::VideoCB(uint8_t *buf, size_t buf_size) { // callback function to decode video buffer @@ -153,7 +183,7 @@ bool IO::VideoCB(uint8_t * buf, size_t buf_size) av_init_packet(&packet); packet.data = buf; packet.size = buf_size; - AVFrame * frame = av_frame_alloc(); + AVFrame *frame = av_frame_alloc(); if(!frame) { CHIAKI_LOGE(this->log, "UpdateFrame Failed to alloc AVFrame"); @@ -204,7 +234,6 @@ send_packet: return true; } - void IO::InitAudioCB(unsigned int channels, unsigned int rate) { SDL_AudioSpec want, have, test; @@ -241,12 +270,12 @@ void IO::InitAudioCB(unsigned int channels, unsigned int rate) } } -void IO::AudioCB(int16_t * buf, size_t samples_count) +void IO::AudioCB(int16_t *buf, size_t samples_count) { - for(int x=0; x < samples_count*2; x++) + for(int x = 0; x < samples_count * 2; x++) { // boost audio volume - int sample = buf[x]*1.80; + int sample = buf[x] * 1.80; // Hard clipping (audio compression) // truncate value that overflow/underflow int16 if(sample > INT16_MAX) @@ -260,9 +289,9 @@ void IO::AudioCB(int16_t * buf, size_t samples_count) CHIAKI_LOGD(this->log, "Audio Hard clipping INT16_MIN > %d", sample); } else - buf[x] = (int16_t) sample; + buf[x] = (int16_t)sample; } - int success = SDL_QueueAudio(this->sdl_audio_device_id, buf, sizeof(int16_t)*samples_count*2); + int success = SDL_QueueAudio(this->sdl_audio_device_id, buf, sizeof(int16_t) * samples_count * 2); if(success != 0) CHIAKI_LOGE(this->log, "SDL_QueueAudio failed: %s\n", SDL_GetError()); } @@ -289,33 +318,6 @@ bool IO::InitVideo(int video_width, int video_height, int screen_width, int scre return true; } -void IO::EventCB(ChiakiEvent *event) -{ - switch(event->type) - { - case CHIAKI_EVENT_CONNECTED: - CHIAKI_LOGI(this->log, "EventCB CHIAKI_EVENT_CONNECTED"); - if(this->chiaki_event_connected_cb != nullptr) - this->quit = !this->chiaki_event_connected_cb(); - else - this->quit = false; - break; - case CHIAKI_EVENT_LOGIN_PIN_REQUEST: - CHIAKI_LOGI(this->log, "EventCB CHIAKI_EVENT_LOGIN_PIN_REQUEST"); - if(this->chiaki_even_login_pin_request_cb != nullptr) - this->quit = !this->chiaki_even_login_pin_request_cb(event->login_pin_request.pin_incorrect); - break; - case CHIAKI_EVENT_QUIT: - CHIAKI_LOGI(this->log, "EventCB CHIAKI_EVENT_QUIT"); - if(this->chiaki_event_quit_cb != nullptr) - this->quit = !this->chiaki_event_quit_cb(&event->quit); - else - this->quit = true; - break; - } -} - - bool IO::FreeVideo() { bool ret = true; @@ -344,12 +346,12 @@ bool IO::ReadUserKeyboard(char *buffer, size_t buffer_size) SwkbdConfig kbd; Result rc = swkbdCreate(&kbd, 0); - if (R_SUCCEEDED(rc)) + if(R_SUCCEEDED(rc)) { swkbdConfigMakePresetDefault(&kbd); rc = swkbdShow(&kbd, buffer, buffer_size); - if (R_SUCCEEDED(rc)) + if(R_SUCCEEDED(rc)) { CHIAKI_LOGI(this->log, "Got user input: %s\n", buffer); } @@ -377,7 +379,7 @@ bool IO::ReadGameTouchScreen(ChiakiControllerState *state) bool ret = false; if(!touch_count) { - for(int i=0; i < CHIAKI_CONTROLLER_TOUCHES_MAX; i++) + for(int i = 0; i < CHIAKI_CONTROLLER_TOUCHES_MAX; i++) { if(state->touches[i].id != -1) { @@ -393,7 +395,7 @@ bool IO::ReadGameTouchScreen(ChiakiControllerState *state) } touchPosition touch; - for(int i=0; i < touch_count && i < CHIAKI_CONTROLLER_TOUCHES_MAX; i++) + for(int i = 0; i < touch_count && i < CHIAKI_CONTROLLER_TOUCHES_MAX; i++) { hidTouchRead(&touch, i); @@ -403,11 +405,10 @@ bool IO::ReadGameTouchScreen(ChiakiControllerState *state) uint16_t y = touch.py * (DS4_TRACKPAD_MAX_Y / SWITCH_TOUCHSCREEN_MAX_Y); // use nintendo switch border's 5% to - if(x <= (SWITCH_TOUCHSCREEN_MAX_X * 0.05) || x >= (SWITCH_TOUCHSCREEN_MAX_X * 0.95) - || y <= (SWITCH_TOUCHSCREEN_MAX_Y * 0.05) || y >= (SWITCH_TOUCHSCREEN_MAX_Y * 0.95)) + if(x <= (SWITCH_TOUCHSCREEN_MAX_X * 0.05) || x >= (SWITCH_TOUCHSCREEN_MAX_X * 0.95) || y <= (SWITCH_TOUCHSCREEN_MAX_Y * 0.05) || y >= (SWITCH_TOUCHSCREEN_MAX_Y * 0.95)) { state->buttons |= CHIAKI_CONTROLLER_BUTTON_TOUCHPAD; // touchscreen - // printf("CHIAKI_CONTROLLER_BUTTON_TOUCHPAD\n"); + // printf("CHIAKI_CONTROLLER_BUTTON_TOUCHPAD\n"); } else { @@ -459,7 +460,7 @@ bool IO::ReadGameKeys(SDL_Event *event, ChiakiControllerState *state) else ret = false; } - else if (event->jaxis.which == 1) + else if(event->jaxis.which == 1) { // right joystick if(event->jaxis.axis == 0) @@ -479,26 +480,58 @@ bool IO::ReadGameKeys(SDL_Event *event, ChiakiControllerState *state) // event->jbutton.which, event->jbutton.button); switch(event->jbutton.button) { - case 0: state->buttons |= CHIAKI_CONTROLLER_BUTTON_MOON; break; // KEY_A - case 1: state->buttons |= CHIAKI_CONTROLLER_BUTTON_CROSS; break; // KEY_B - case 2: state->buttons |= CHIAKI_CONTROLLER_BUTTON_PYRAMID; break; // KEY_X - case 3: state->buttons |= CHIAKI_CONTROLLER_BUTTON_BOX; break; // KEY_Y - case 12: state->buttons |= CHIAKI_CONTROLLER_BUTTON_DPAD_LEFT; break; // KEY_DLEFT - case 14: state->buttons |= CHIAKI_CONTROLLER_BUTTON_DPAD_RIGHT; break; // KEY_DRIGHT - case 13: state->buttons |= CHIAKI_CONTROLLER_BUTTON_DPAD_UP; break; // KEY_DUP - case 15: state->buttons |= CHIAKI_CONTROLLER_BUTTON_DPAD_DOWN; break; // KEY_DDOWN - case 6: state->buttons |= CHIAKI_CONTROLLER_BUTTON_L1; break; // KEY_L - case 7: state->buttons |= CHIAKI_CONTROLLER_BUTTON_R1; break; // KEY_R - case 8: state->l2_state = 0xff; break; // KEY_ZL - case 9: state->r2_state = 0xff; break; // KEY_ZR - case 4: state->buttons |= CHIAKI_CONTROLLER_BUTTON_L3; break; // KEY_LSTICK - case 5: state->buttons |= CHIAKI_CONTROLLER_BUTTON_R3; break; // KEY_RSTICK - case 10: state->buttons |= CHIAKI_CONTROLLER_BUTTON_OPTIONS; break; // KEY_PLUS + case 0: + state->buttons |= CHIAKI_CONTROLLER_BUTTON_MOON; + break; // KEY_A + case 1: + state->buttons |= CHIAKI_CONTROLLER_BUTTON_CROSS; + break; // KEY_B + case 2: + state->buttons |= CHIAKI_CONTROLLER_BUTTON_PYRAMID; + break; // KEY_X + case 3: + state->buttons |= CHIAKI_CONTROLLER_BUTTON_BOX; + break; // KEY_Y + case 12: + state->buttons |= CHIAKI_CONTROLLER_BUTTON_DPAD_LEFT; + break; // KEY_DLEFT + case 14: + state->buttons |= CHIAKI_CONTROLLER_BUTTON_DPAD_RIGHT; + break; // KEY_DRIGHT + case 13: + state->buttons |= CHIAKI_CONTROLLER_BUTTON_DPAD_UP; + break; // KEY_DUP + case 15: + state->buttons |= CHIAKI_CONTROLLER_BUTTON_DPAD_DOWN; + break; // KEY_DDOWN + case 6: + state->buttons |= CHIAKI_CONTROLLER_BUTTON_L1; + break; // KEY_L + case 7: + state->buttons |= CHIAKI_CONTROLLER_BUTTON_R1; + break; // KEY_R + case 8: + state->l2_state = 0xff; + break; // KEY_ZL + case 9: + state->r2_state = 0xff; + break; // KEY_ZR + case 4: + state->buttons |= CHIAKI_CONTROLLER_BUTTON_L3; + break; // KEY_LSTICK + case 5: + state->buttons |= CHIAKI_CONTROLLER_BUTTON_R3; + break; // KEY_RSTICK + case 10: + state->buttons |= CHIAKI_CONTROLLER_BUTTON_OPTIONS; + break; // KEY_PLUS // FIXME - // case 11: state->buttons |= CHIAKI_CONTROLLER_BUTTON_SHARE; break; // KEY_MINUS - case 11: state->buttons |= CHIAKI_CONTROLLER_BUTTON_PS; break; // KEY_MINUS + // case 11: state->buttons |= CHIAKI_CONTROLLER_BUTTON_SHARE; break; // KEY_MINUS + case 11: + state->buttons |= CHIAKI_CONTROLLER_BUTTON_PS; + break; // KEY_MINUS default: - ret = false; + ret = false; } break; case SDL_JOYBUTTONUP: @@ -506,25 +539,57 @@ bool IO::ReadGameKeys(SDL_Event *event, ChiakiControllerState *state) // event->jbutton.which, event->jbutton.button); switch(event->jbutton.button) { - case 0: state->buttons ^= CHIAKI_CONTROLLER_BUTTON_MOON; break; // KEY_A - case 1: state->buttons ^= CHIAKI_CONTROLLER_BUTTON_CROSS; break; // KEY_B - case 2: state->buttons ^= CHIAKI_CONTROLLER_BUTTON_PYRAMID; break; // KEY_X - case 3: state->buttons ^= CHIAKI_CONTROLLER_BUTTON_BOX; break; // KEY_Y - case 12: state->buttons ^= CHIAKI_CONTROLLER_BUTTON_DPAD_LEFT; break; // KEY_DLEFT - case 14: state->buttons ^= CHIAKI_CONTROLLER_BUTTON_DPAD_RIGHT; break; // KEY_DRIGHT - case 13: state->buttons ^= CHIAKI_CONTROLLER_BUTTON_DPAD_UP; break; // KEY_DUP - case 15: state->buttons ^= CHIAKI_CONTROLLER_BUTTON_DPAD_DOWN; break; // KEY_DDOWN - case 6: state->buttons ^= CHIAKI_CONTROLLER_BUTTON_L1; break; // KEY_L - case 7: state->buttons ^= CHIAKI_CONTROLLER_BUTTON_R1; break; // KEY_R - case 8: state->l2_state = 0x00; break; // KEY_ZL - case 9: state->r2_state = 0x00; break; // KEY_ZR - case 4: state->buttons ^= CHIAKI_CONTROLLER_BUTTON_L3; break; // KEY_LSTICK - case 5: state->buttons ^= CHIAKI_CONTROLLER_BUTTON_R3; break; // KEY_RSTICK - case 10: state->buttons ^= CHIAKI_CONTROLLER_BUTTON_OPTIONS; break; // KEY_PLUS - //case 11: state->buttons ^= CHIAKI_CONTROLLER_BUTTON_SHARE; break; // KEY_MINUS - case 11: state->buttons ^= CHIAKI_CONTROLLER_BUTTON_PS; break; // KEY_MINUS + case 0: + state->buttons ^= CHIAKI_CONTROLLER_BUTTON_MOON; + break; // KEY_A + case 1: + state->buttons ^= CHIAKI_CONTROLLER_BUTTON_CROSS; + break; // KEY_B + case 2: + state->buttons ^= CHIAKI_CONTROLLER_BUTTON_PYRAMID; + break; // KEY_X + case 3: + state->buttons ^= CHIAKI_CONTROLLER_BUTTON_BOX; + break; // KEY_Y + case 12: + state->buttons ^= CHIAKI_CONTROLLER_BUTTON_DPAD_LEFT; + break; // KEY_DLEFT + case 14: + state->buttons ^= CHIAKI_CONTROLLER_BUTTON_DPAD_RIGHT; + break; // KEY_DRIGHT + case 13: + state->buttons ^= CHIAKI_CONTROLLER_BUTTON_DPAD_UP; + break; // KEY_DUP + case 15: + state->buttons ^= CHIAKI_CONTROLLER_BUTTON_DPAD_DOWN; + break; // KEY_DDOWN + case 6: + state->buttons ^= CHIAKI_CONTROLLER_BUTTON_L1; + break; // KEY_L + case 7: + state->buttons ^= CHIAKI_CONTROLLER_BUTTON_R1; + break; // KEY_R + case 8: + state->l2_state = 0x00; + break; // KEY_ZL + case 9: + state->r2_state = 0x00; + break; // KEY_ZR + case 4: + state->buttons ^= CHIAKI_CONTROLLER_BUTTON_L3; + break; // KEY_LSTICK + case 5: + state->buttons ^= CHIAKI_CONTROLLER_BUTTON_R3; + break; // KEY_RSTICK + case 10: + state->buttons ^= CHIAKI_CONTROLLER_BUTTON_OPTIONS; + break; // KEY_PLUS + //case 11: state->buttons ^= CHIAKI_CONTROLLER_BUTTON_SHARE; break; // KEY_MINUS + case 11: + state->buttons ^= CHIAKI_CONTROLLER_BUTTON_PS; + break; // KEY_MINUS default: - ret = false; + ret = false; } break; default: @@ -582,7 +647,7 @@ bool IO::InitOpenGlTextures() D(glGenTextures(PLANES_COUNT, this->tex)); D(glGenBuffers(PLANES_COUNT, this->pbo)); uint8_t uv_default[] = {0x7f, 0x7f}; - for(int i=0; i < PLANES_COUNT; i++) + for(int i = 0; i < PLANES_COUNT; i++) { D(glBindTexture(GL_TEXTURE_2D, this->tex[i])); D(glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR)); @@ -595,7 +660,7 @@ bool IO::InitOpenGlTextures() D(glUseProgram(this->prog)); // bind only as many planes as we need const char *plane_names[] = {"plane1", "plane2", "plane3"}; - for(int i=0; i < PLANES_COUNT; i++) + for(int i = 0; i < PLANES_COUNT; i++) D(glUniform1i(glGetUniformLocation(this->prog, plane_names[i]), i)); D(glGenVertexArrays(1, &this->vao)); @@ -615,14 +680,14 @@ bool IO::InitOpenGlTextures() return true; } -GLuint IO::CreateAndCompileShader(GLenum type, const char* source) +GLuint IO::CreateAndCompileShader(GLenum type, const char *source) { GLint success; GLchar msg[512]; GLuint handle; D(handle = glCreateShader(type)); - if (!handle) + if(!handle) { CHIAKI_LOGE(this->log, "%u: cannot create shader", type); DP(this->prog); @@ -632,7 +697,7 @@ GLuint IO::CreateAndCompileShader(GLenum type, const char* source) D(glCompileShader(handle)); D(glGetShaderiv(handle, GL_COMPILE_STATUS, &success)); - if (!success) + if(!success) { D(glGetShaderInfoLog(handle, sizeof(msg), nullptr, msg)); CHIAKI_LOGE(this->log, "%u: %s\n", type, msg); @@ -658,7 +723,7 @@ bool IO::InitOpenGlShader() GLint success; D(glGetProgramiv(this->prog, GL_LINK_STATUS, &success)); - if (!success) + if(!success) { char buf[512]; glGetProgramInfoLog(this->prog, sizeof(buf), nullptr, buf); @@ -672,15 +737,15 @@ bool IO::InitOpenGlShader() return true; } -inline void IO::SetOpenGlYUVPixels(AVFrame * frame) +inline void IO::SetOpenGlYUVPixels(AVFrame *frame) { D(glUseProgram(this->prog)); int planes[][3] = { // { width_divide, height_divider, data_per_pixel } - { 1, 1, 1 }, // Y - { 2, 2, 1 }, // U - { 2, 2, 1 } // V + {1, 1, 1}, // Y + {2, 2, 1}, // U + {2, 2, 1} // V }; this->mtx.lock(); @@ -689,7 +754,7 @@ inline void IO::SetOpenGlYUVPixels(AVFrame * frame) int width = frame->width / planes[i][0]; int height = frame->height / planes[i][1]; int size = width * height * planes[i][2]; - uint8_t * buf; + uint8_t *buf; D(glBindBuffer(GL_PIXEL_UNPACK_BUFFER, this->pbo[i])); D(glBufferData(GL_PIXEL_UNPACK_BUFFER, size, nullptr, GL_STREAM_DRAW)); @@ -700,7 +765,7 @@ inline void IO::SetOpenGlYUVPixels(AVFrame * frame) D(glGetBufferParameteriv(GL_PIXEL_UNPACK_BUFFER, GL_BUFFER_SIZE, &data)); CHIAKI_LOGE(this->log, "AVOpenGLFrame failed to map PBO"); CHIAKI_LOGE(this->log, "Info buf == %p. size %d frame %d * %d, divs %d, %d, pbo %d GL_BUFFER_SIZE %x", - buf, size, frame->width, frame->height, planes[i][0], planes[i][1], pbo[i], data); + buf, size, frame->width, frame->height, planes[i][0], planes[i][1], pbo[i], data); continue; } @@ -712,10 +777,10 @@ inline void IO::SetOpenGlYUVPixels(AVFrame * frame) else { // UV - for(int l=0; ldata[i] + frame->linesize[i] * l, - width * planes[i][2]); + frame->data[i] + frame->linesize[i] * l, + width * planes[i][2]); } D(glUnmapBuffer(GL_PIXEL_UNPACK_BUFFER)); D(glBindTexture(GL_TEXTURE_2D, tex[i])); @@ -736,7 +801,7 @@ inline void IO::OpenGlDraw() //avcodec_flush_buffers(this->codec_context); D(glBindVertexArray(this->vao)); - for(int i=0; i< PLANES_COUNT; i++) + for(int i = 0; i < PLANES_COUNT; i++) { D(glActiveTexture(GL_TEXTURE0 + i)); D(glBindTexture(GL_TEXTURE_2D, this->tex[i])); @@ -753,10 +818,10 @@ bool IO::InitJoystick() // open CONTROLLER_PLAYER_1 and CONTROLLER_PLAYER_2 // when railed, both joycons are mapped to joystick #0, // else joycons are individually mapped to joystick #0, joystick #1, ... - for (int i = 0; i < SDL_JOYSTICK_COUNT; i++) + for(int i = 0; i < SDL_JOYSTICK_COUNT; i++) { this->sdl_joystick_ptr[i] = SDL_JoystickOpen(i); - if (sdl_joystick_ptr[i] == nullptr) + if(sdl_joystick_ptr[i] == nullptr) { CHIAKI_LOGE(this->log, "SDL_JoystickOpen: %s\n", SDL_GetError()); return false; @@ -767,7 +832,7 @@ bool IO::InitJoystick() bool IO::FreeJoystick() { - for (int i = 0; i < SDL_JOYSTICK_COUNT; i++) + for(int i = 0; i < SDL_JOYSTICK_COUNT; i++) { if(SDL_JoystickGetAttached(sdl_joystick_ptr[i])) SDL_JoystickClose(sdl_joystick_ptr[i]); @@ -775,7 +840,7 @@ bool IO::FreeJoystick() return true; } -bool IO::MainLoop(ChiakiControllerState * state) +bool IO::MainLoop(ChiakiControllerState *state) { D(glUseProgram(this->prog)); @@ -798,4 +863,3 @@ bool IO::MainLoop(ChiakiControllerState * state) return !this->quit; } - diff --git a/switch/src/main.cpp b/switch/src/main.cpp index bdba8a8..c12d413 100644 --- a/switch/src/main.cpp +++ b/switch/src/main.cpp @@ -134,9 +134,8 @@ int main(int argc, char * argv[]) } // build sdl OpenGl and AV decoders graphical interface - IO io = IO(log); // open Input Output class DiscoveryManager discoverymanager = DiscoveryManager(); - MainApplication app = MainApplication(&discoverymanager, &io); + MainApplication app = MainApplication(&discoverymanager); app.Load(); CHIAKI_LOGI(log, "Quit applet"); diff --git a/switch/src/settings.cpp b/switch/src/settings.cpp index b1878ca..f0eb3e3 100644 --- a/switch/src/settings.cpp +++ b/switch/src/settings.cpp @@ -6,11 +6,10 @@ Settings::Settings() { -#if defined(__SWITCH__) && !defined(CHIAKI_ENABLE_SWITCH_NXLINK) - // null log for switch version - chiaki_log_init(&this->log, CHIAKI_LOG_ALL ^ CHIAKI_LOG_VERBOSE, chiaki_log_cb_print, NULL); +#if defined(__SWITCH__) + chiaki_log_init(&this->log, CHIAKI_LOG_ALL ^ CHIAKI_LOG_VERBOSE ^ CHIAKI_LOG_DEBUG, chiaki_log_cb_print, NULL); #else - chiaki_log_init(&this->log, CHIAKI_LOG_ALL ^ CHIAKI_LOG_VERBOSE, chiaki_log_cb_print, NULL); + chiaki_log_init(&this->log, CHIAKI_LOG_ALL, chiaki_log_cb_print, NULL); #endif } From c622f418e452939a6dbd0833fecfb0919e60ac23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Sun, 27 Dec 2020 19:01:58 +0100 Subject: [PATCH 116/237] Fix Feedback State --- lib/src/takion.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/src/takion.c b/lib/src/takion.c index 934f151..48a1537 100644 --- a/lib/src/takion.c +++ b/lib/src/takion.c @@ -555,13 +555,13 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_takion_send_feedback_state(ChiakiTakion *ta size_t buf_sz; if(takion->version <= 9) { - buf_sz = CHIAKI_FEEDBACK_STATE_BUF_SIZE_V9; + buf_sz = 0xc + CHIAKI_FEEDBACK_STATE_BUF_SIZE_V9; chiaki_feedback_state_format_v9(buf + 0xc, feedback_state); } else { - buf_sz = CHIAKI_FEEDBACK_STATE_BUF_SIZE_V12; - chiaki_feedback_state_format_v9(buf + 0xc, feedback_state); + buf_sz = 0xc + CHIAKI_FEEDBACK_STATE_BUF_SIZE_V12; + chiaki_feedback_state_format_v12(buf + 0xc, feedback_state); } return takion_send_feedback_packet(takion, buf, buf_sz); } From 3417202049bb8fff30768b07d67979a3392b4c35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Sun, 27 Dec 2020 19:11:12 +0100 Subject: [PATCH 117/237] Mark Registered Console Type in GUI --- gui/src/settingsdialog.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/gui/src/settingsdialog.cpp b/gui/src/settingsdialog.cpp index 6eb9d98..c93e63b 100644 --- a/gui/src/settingsdialog.cpp +++ b/gui/src/settingsdialog.cpp @@ -348,7 +348,10 @@ void SettingsDialog::UpdateRegisteredHosts() auto hosts = settings->GetRegisteredHosts(); for(const auto &host : hosts) { - auto item = new QListWidgetItem(QString("%1 (%2)").arg(host.GetServerMAC().ToString(), host.GetServerNickname())); + auto item = new QListWidgetItem(QString("%1 (%2, %3)") + .arg(host.GetServerMAC().ToString(), + chiaki_target_is_ps5(host.GetTarget()) ? "PS5" : "PS4", + host.GetServerNickname())); item->setData(Qt::UserRole, QVariant::fromValue(host.GetServerMAC())); registered_hosts_list_widget->addItem(item); } From c19c7869d5c0868f38d07968816d8028873186ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Mon, 28 Dec 2020 17:07:37 +0100 Subject: [PATCH 118/237] Add Video Profile Auto Downgrade --- gui/src/streamsession.cpp | 1 + lib/include/chiaki/session.h | 2 ++ lib/src/ctrl.c | 19 ++++++++++++++++++- lib/src/session.c | 1 + switch/src/host.cpp | 3 ++- 5 files changed, 24 insertions(+), 2 deletions(-) diff --git a/gui/src/streamsession.cpp b/gui/src/streamsession.cpp index fa696be..e1b8cd6 100644 --- a/gui/src/streamsession.cpp +++ b/gui/src/streamsession.cpp @@ -105,6 +105,7 @@ StreamSession::StreamSession(const StreamSessionConnectInfo &connect_info, QObje chiaki_connect_info.ps5 = chiaki_target_is_ps5(connect_info.target); chiaki_connect_info.host = host_str.constData(); chiaki_connect_info.video_profile = connect_info.video_profile; + chiaki_connect_info.video_profile_auto_downgrade = true; if(connect_info.regist_key.size() != sizeof(chiaki_connect_info.regist_key)) throw ChiakiException("RegistKey invalid"); diff --git a/lib/include/chiaki/session.h b/lib/include/chiaki/session.h index e036cf7..db7bf02 100644 --- a/lib/include/chiaki/session.h +++ b/lib/include/chiaki/session.h @@ -76,6 +76,7 @@ typedef struct chiaki_connect_info_t char regist_key[CHIAKI_SESSION_AUTH_SIZE]; // must be completely filled (pad with \0) uint8_t morning[0x10]; ChiakiConnectVideoProfile video_profile; + bool video_profile_auto_downgrade; // Downgrade video_profile if server does not seem to support it. bool enable_keyboard; } ChiakiConnectInfo; @@ -159,6 +160,7 @@ typedef struct chiaki_session_t uint8_t morning[CHIAKI_RPCRYPT_KEY_SIZE]; uint8_t did[CHIAKI_RP_DID_SIZE]; ChiakiConnectVideoProfile video_profile; + bool video_profile_auto_downgrade; bool enable_keyboard; } connect_info; diff --git a/lib/src/ctrl.c b/lib/src/ctrl.c index b910fc9..a3b8461 100644 --- a/lib/src/ctrl.c +++ b/lib/src/ctrl.c @@ -963,7 +963,24 @@ static ChiakiErrorCode ctrl_connect(ChiakiCtrl *ctrl) } } - if(!response.server_type_valid) + if(response.server_type_valid) + { + uint8_t server_type = response.rp_server_type[0]; // 0 = PS4, 1 = PS4 Pro, 2 = PS5 + CHIAKI_LOGI(session->log, "Ctrl got Server Type: %u", (unsigned int)server_type); + if(server_type == 0 + && session->connect_info.video_profile_auto_downgrade + && session->connect_info.video_profile.height == 1080) + { + CHIAKI_LOGI(session->log, "1080p was selected but server would not support it. Downgrading."); + chiaki_connect_video_profile_preset( + &session->connect_info.video_profile, + CHIAKI_VIDEO_RESOLUTION_PRESET_720p, + session->connect_info.video_profile.max_fps == 60 + ? CHIAKI_VIDEO_FPS_PRESET_60 + : CHIAKI_VIDEO_FPS_PRESET_30); + } + } + else CHIAKI_LOGE(session->log, "No valid Server Type in ctrl response"); ctrl->sock = sock; diff --git a/lib/src/session.c b/lib/src/session.c index 84177ed..1c0c388 100644 --- a/lib/src/session.c +++ b/lib/src/session.c @@ -224,6 +224,7 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_session_init(ChiakiSession *session, Chiaki memcpy(session->connect_info.did + sizeof(session->connect_info.did) - sizeof(did_suffix), did_suffix, sizeof(did_suffix)); session->connect_info.video_profile = connect_info->video_profile; + session->connect_info.video_profile_auto_downgrade = connect_info->video_profile_auto_downgrade; session->connect_info.enable_keyboard = connect_info->enable_keyboard; return CHIAKI_ERR_SUCCESS; diff --git a/switch/src/host.cpp b/switch/src/host.cpp index 0770ca0..6db9c50 100644 --- a/switch/src/host.cpp +++ b/switch/src/host.cpp @@ -139,10 +139,11 @@ int Host::InitSession(IO *user) // Build chiaki ps4 stream session chiaki_opus_decoder_init(&(this->opus_decoder), this->log); ChiakiAudioSink audio_sink; - ChiakiConnectInfo chiaki_connect_info = {0}; + ChiakiConnectInfo chiaki_connect_info = {}; chiaki_connect_info.host = this->host_addr.c_str(); chiaki_connect_info.video_profile = this->video_profile; + chiaki_connect_info.video_profile_auto_downgrade = true; chiaki_connect_info.ps5 = this->IsPS5(); From e6d18155af4b3ac98dcec37ec2321d29e69854b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Mon, 28 Dec 2020 17:27:10 +0100 Subject: [PATCH 119/237] Add H265 Streaming to GUI --- gui/include/settings.h | 3 +++ gui/include/settingsdialog.h | 2 ++ gui/src/settings.cpp | 20 +++++++++++++++++++- gui/src/settingsdialog.cpp | 20 ++++++++++++++++++++ gui/src/streamsession.cpp | 2 +- lib/include/chiaki/common.h | 5 +++-- lib/src/ctrl.c | 8 ++++++++ lib/src/session.c | 1 + 8 files changed, 57 insertions(+), 4 deletions(-) diff --git a/gui/include/settings.h b/gui/include/settings.h index dc8db49..c2320b8 100644 --- a/gui/include/settings.h +++ b/gui/include/settings.h @@ -75,6 +75,9 @@ class Settings : public QObject unsigned int GetBitrate() const; void SetBitrate(unsigned int bitrate); + ChiakiCodec GetCodec() const; + void SetCodec(ChiakiCodec codec); + Decoder GetDecoder() const; void SetDecoder(Decoder decoder); diff --git a/gui/include/settingsdialog.h b/gui/include/settingsdialog.h index 5b741dd..d564bef 100644 --- a/gui/include/settingsdialog.h +++ b/gui/include/settingsdialog.h @@ -24,6 +24,7 @@ class SettingsDialog : public QDialog QComboBox *resolution_combo_box; QComboBox *fps_combo_box; QLineEdit *bitrate_edit; + QComboBox *codec_combo_box; QLineEdit *audio_buffer_size_edit; QComboBox *audio_device_combo_box; QCheckBox *pi_decoder_check_box; @@ -41,6 +42,7 @@ class SettingsDialog : public QDialog void ResolutionSelected(); void FPSSelected(); void BitrateEdited(); + void CodecSelected(); void AudioBufferSizeEdited(); void AudioOutputSelected(); void HardwareDecodeEngineSelected(); diff --git a/gui/src/settings.cpp b/gui/src/settings.cpp index c8c2a9d..715c2d5 100644 --- a/gui/src/settings.cpp +++ b/gui/src/settings.cpp @@ -132,6 +132,24 @@ void Settings::SetBitrate(unsigned int bitrate) settings.setValue("settings/bitrate", bitrate); } +static const QMap codecs = { + { CHIAKI_CODEC_H264, "h264"}, + { CHIAKI_CODEC_H265, "h265"} +}; + +static const ChiakiCodec codec_default = CHIAKI_CODEC_H265; + +ChiakiCodec Settings::GetCodec() const +{ + auto v = settings.value("settings/codec", codecs[codec_default]).toString(); + return codecs.key(v, codec_default); +} + +void Settings::SetCodec(ChiakiCodec codec) +{ + settings.setValue("settings/codec", codecs[codec]); +} + unsigned int Settings::GetAudioBufferSizeDefault() const { return 9600; @@ -202,7 +220,7 @@ ChiakiConnectVideoProfile Settings::GetVideoProfile() unsigned int bitrate = GetBitrate(); if(bitrate) profile.bitrate = bitrate; - profile.codec = CHIAKI_CODEC_H264; // TODO: add a setting + profile.codec = GetCodec(); return profile; } diff --git a/gui/src/settingsdialog.cpp b/gui/src/settingsdialog.cpp index c93e63b..b8bd412 100644 --- a/gui/src/settingsdialog.cpp +++ b/gui/src/settingsdialog.cpp @@ -174,6 +174,21 @@ SettingsDialog::SettingsDialog(Settings *settings, QWidget *parent) : QDialog(pa connect(bitrate_edit, &QLineEdit::textEdited, this, &SettingsDialog::BitrateEdited); UpdateBitratePlaceholder(); + codec_combo_box = new QComboBox(this); + static const QList> codec_strings = { + { CHIAKI_CODEC_H264, "H264" }, + { CHIAKI_CODEC_H265, "H265 (PS5 only)" } + }; + auto current_codec = settings->GetCodec(); + for(const auto &p : codec_strings) + { + codec_combo_box->addItem(p.second, (int)p.first); + if(current_codec == p.first) + codec_combo_box->setCurrentIndex(codec_combo_box->count() - 1); + } + connect(codec_combo_box, SIGNAL(currentIndexChanged(int)), this, SLOT(CodecSelected())); + stream_settings_layout->addRow(tr("Codec:"), codec_combo_box); + audio_buffer_size_edit = new QLineEdit(this); audio_buffer_size_edit->setValidator(new QIntValidator(1024, 0x20000, audio_buffer_size_edit)); unsigned int audio_buffer_size = settings->GetAudioBufferSizeRaw(); @@ -317,6 +332,11 @@ void SettingsDialog::BitrateEdited() settings->SetBitrate(bitrate_edit->text().toUInt()); } +void SettingsDialog::CodecSelected() +{ + settings->SetCodec((ChiakiCodec)codec_combo_box->currentData().toInt()); +} + void SettingsDialog::AudioBufferSizeEdited() { settings->SetAudioBufferSize(audio_buffer_size_edit->text().toUInt()); diff --git a/gui/src/streamsession.cpp b/gui/src/streamsession.cpp index e1b8cd6..fedae10 100644 --- a/gui/src/streamsession.cpp +++ b/gui/src/streamsession.cpp @@ -68,7 +68,7 @@ StreamSession::StreamSession(const StreamSessionConnectInfo &connect_info, QObje chiaki_log_sniffer_init(&sniffer, CHIAKI_LOG_ALL, GetChiakiLog()); ChiakiErrorCode err = chiaki_ffmpeg_decoder_init(ffmpeg_decoder, chiaki_log_sniffer_get_log(&sniffer), - connect_info.video_profile.codec, + chiaki_target_is_ps5(connect_info.target) ? connect_info.video_profile.codec : CHIAKI_CODEC_H264, connect_info.hw_decoder.isEmpty() ? NULL : connect_info.hw_decoder.toUtf8().constData(), FfmpegFrameCb, this); if(err != CHIAKI_ERR_SUCCESS) diff --git a/lib/include/chiaki/common.h b/lib/include/chiaki/common.h index 74bb344..630c954 100644 --- a/lib/include/chiaki/common.h +++ b/lib/include/chiaki/common.h @@ -85,9 +85,10 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_lib_init(); typedef enum { + // values must not change CHIAKI_CODEC_H264 = 0, - CHIAKI_CODEC_H265, - CHIAKI_CODEC_H265_HDR + CHIAKI_CODEC_H265 = 1, + CHIAKI_CODEC_H265_HDR = 2 } ChiakiCodec; static inline bool chiaki_codec_is_h265(ChiakiCodec codec) diff --git a/lib/src/ctrl.c b/lib/src/ctrl.c index a3b8461..f92d12b 100644 --- a/lib/src/ctrl.c +++ b/lib/src/ctrl.c @@ -971,6 +971,7 @@ static ChiakiErrorCode ctrl_connect(ChiakiCtrl *ctrl) && session->connect_info.video_profile_auto_downgrade && session->connect_info.video_profile.height == 1080) { + // regular PS4 doesn't support >= 1080p CHIAKI_LOGI(session->log, "1080p was selected but server would not support it. Downgrading."); chiaki_connect_video_profile_preset( &session->connect_info.video_profile, @@ -979,6 +980,13 @@ static ChiakiErrorCode ctrl_connect(ChiakiCtrl *ctrl) ? CHIAKI_VIDEO_FPS_PRESET_60 : CHIAKI_VIDEO_FPS_PRESET_30); } + if((server_type == 0 || server_type == 1) + && session->connect_info.video_profile.codec != CHIAKI_CODEC_H264) + { + // PS4 doesn't support anything except h264 + CHIAKI_LOGI(session->log, "A codec other than H264 was selected but server would not support it. Downgrading."); + session->connect_info.video_profile.codec = CHIAKI_CODEC_H264; + } } else CHIAKI_LOGE(session->log, "No valid Server Type in ctrl response"); diff --git a/lib/src/session.c b/lib/src/session.c index 1c0c388..de2f4e2 100644 --- a/lib/src/session.c +++ b/lib/src/session.c @@ -88,6 +88,7 @@ CHIAKI_EXPORT ChiakiTarget chiaki_rp_version_parse(const char *rp_version_str, b CHIAKI_EXPORT void chiaki_connect_video_profile_preset(ChiakiConnectVideoProfile *profile, ChiakiVideoResolutionPreset resolution, ChiakiVideoFPSPreset fps) { + profile->codec = CHIAKI_CODEC_H264; switch(resolution) { case CHIAKI_VIDEO_RESOLUTION_PRESET_360p: From bcda423db7acc6928ab33ee340babb01e3107b22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Mon, 28 Dec 2020 19:33:38 +0100 Subject: [PATCH 120/237] Fix Android Build --- android/app/src/main/cpp/chiaki-jni.c | 17 ++++++++--- .../com/metallic/chiaki/common/AppDatabase.kt | 25 ++++++++++++++-- .../com/metallic/chiaki/common/DisplayHost.kt | 8 ++--- .../metallic/chiaki/common/RegisteredHost.kt | 25 +++++++++------- .../chiaki/common/SerializedSettings.kt | 30 +++++++++++-------- .../chiaki/discovery/DiscoveryManager.kt | 4 +-- .../java/com/metallic/chiaki/lib/Chiaki.kt | 24 +++++++++++---- .../com/metallic/chiaki/main/MainActivity.kt | 2 +- .../com/metallic/chiaki/main/MainViewModel.kt | 2 +- .../EditManualConsoleActivity.kt | 2 +- .../chiaki/regist/RegistExecuteActivity.kt | 2 +- .../chiaki/regist/RegistExecuteViewModel.kt | 4 +-- .../SettingsRegisteredHostsAdapter.kt | 4 +-- .../SettingsRegisteredHostsFragment.kt | 2 +- 14 files changed, 101 insertions(+), 50 deletions(-) diff --git a/android/app/src/main/cpp/chiaki-jni.c b/android/app/src/main/cpp/chiaki-jni.c index 21fab93..a65c2e3 100644 --- a/android/app/src/main/cpp/chiaki-jni.c +++ b/android/app/src/main/cpp/chiaki-jni.c @@ -574,11 +574,11 @@ JNIEXPORT void JNICALL JNI_FCN(discoveryServiceFree)(JNIEnv *env, jobject obj, j free(service); } -JNIEXPORT jint JNICALL JNI_FCN(discoveryServiceWakeup)(JNIEnv *env, jobject obj, jlong ptr, jstring host_string, jlong user_credential) +JNIEXPORT jint JNICALL JNI_FCN(discoveryServiceWakeup)(JNIEnv *env, jobject obj, jlong ptr, jstring host_string, jlong user_credential, jboolean ps5) { AndroidDiscoveryService *service = (AndroidDiscoveryService *)ptr; const char *host = E->GetStringUTFChars(env, host_string, NULL); - ChiakiErrorCode r = chiaki_discovery_wakeup(&global_log, service ? &service->service.discovery : NULL, host, (uint64_t)user_credential); + ChiakiErrorCode r = chiaki_discovery_wakeup(&global_log, service ? &service->service.discovery : NULL, host, (uint64_t)user_credential, ps5); E->ReleaseStringUTFChars(env, host_string, host); return r; } @@ -601,6 +601,13 @@ typedef struct android_chiaki_regist_t jmethodID java_regist_host_ctor; } AndroidChiakiRegist; +static jobject create_jni_target(JNIEnv *env, ChiakiTarget target) +{ + jclass cls = E->FindClass(env, BASE_PACKAGE"/Target"); + jmethodID meth = E->GetStaticMethodID(env, cls, "fromValue", "(I)L"BASE_PACKAGE"/Target;"); + return E->CallStaticObjectMethod(env, cls, meth, (jint)target); +} + static void android_chiaki_regist_cb(ChiakiRegistEvent *event, void *user) { AndroidChiakiRegist *regist = user; @@ -622,12 +629,13 @@ static void android_chiaki_regist_cb(ChiakiRegistEvent *event, void *user) { ChiakiRegisteredHost *host = event->registered_host; jobject java_host = E->NewObject(env, regist->java_regist_host_class, regist->java_regist_host_ctor, + create_jni_target(env, host->target), jnistr_from_ascii(env, host->ap_ssid), jnistr_from_ascii(env, host->ap_bssid), jnistr_from_ascii(env, host->ap_key), jnistr_from_ascii(env, host->ap_name), - jnibytearray_create(env, host->ps4_mac, sizeof(host->ps4_mac)), - jnistr_from_ascii(env, host->ps4_nickname), + jnibytearray_create(env, host->server_mac, sizeof(host->server_mac)), + jnistr_from_ascii(env, host->server_nickname), jnibytearray_create(env, (const uint8_t *)host->rp_regist_key, sizeof(host->rp_regist_key)), (jint)host->rp_key_type, jnibytearray_create(env, host->rp_key, sizeof(host->rp_key))); @@ -675,6 +683,7 @@ JNIEXPORT void JNICALL JNI_FCN(registStart)(JNIEnv *env, jobject obj, jobject re regist->java_regist_host_class = E->NewGlobalRef(env, E->FindClass(env, BASE_PACKAGE"/RegistHost")); regist->java_regist_host_ctor = E->GetMethodID(env, regist->java_regist_host_class, "", "(" + "L"BASE_PACKAGE"/Target;" // target: Target "Ljava/lang/String;" // apSsid: String "Ljava/lang/String;" // apBssid: String "Ljava/lang/String;" // apKey: String diff --git a/android/app/src/main/java/com/metallic/chiaki/common/AppDatabase.kt b/android/app/src/main/java/com/metallic/chiaki/common/AppDatabase.kt index 5a794f1..4d4738c 100644 --- a/android/app/src/main/java/com/metallic/chiaki/common/AppDatabase.kt +++ b/android/app/src/main/java/com/metallic/chiaki/common/AppDatabase.kt @@ -4,9 +4,12 @@ package com.metallic.chiaki.common import android.content.Context import androidx.room.* +import androidx.room.migration.Migration +import androidx.sqlite.db.SupportSQLiteDatabase +import com.metallic.chiaki.lib.Target @Database( - version = 1, + version = 2, entities = [RegisteredHost::class, ManualHost::class]) @TypeConverters(Converters::class) abstract class AppDatabase: RoomDatabase() @@ -16,6 +19,16 @@ abstract class AppDatabase: RoomDatabase() abstract fun importDao(): ImportDao } +val MIGRATION_1_2 = object : Migration(1, 2) +{ + override fun migrate(database: SupportSQLiteDatabase) + { + database.execSQL("ALTER TABLE registered_host RENAME ps4_mac TO server_mac") + database.execSQL("ALTER TABLE registered_host RENAME ps4_nickname TO server_nickname") + database.execSQL("ALTER TABLE registered_host ADD target INTEGER NOT NULL DEFAULT 1000") + } +} + private var database: AppDatabase? = null fun getDatabase(context: Context): AppDatabase { @@ -25,7 +38,9 @@ fun getDatabase(context: Context): AppDatabase val db = Room.databaseBuilder( context.applicationContext, AppDatabase::class.java, - "chiaki").build() + "chiaki") + .addMigrations(MIGRATION_1_2) + .build() database = db return db } @@ -37,4 +52,10 @@ private class Converters @TypeConverter fun macToValue(addr: MacAddress) = addr.value + + @TypeConverter + fun targetFromValue(v: Int) = Target.fromValue(v) + + @TypeConverter + fun targetToValue(target: Target) = target.value } \ No newline at end of file diff --git a/android/app/src/main/java/com/metallic/chiaki/common/DisplayHost.kt b/android/app/src/main/java/com/metallic/chiaki/common/DisplayHost.kt index f930f39..b7c22eb 100644 --- a/android/app/src/main/java/com/metallic/chiaki/common/DisplayHost.kt +++ b/android/app/src/main/java/com/metallic/chiaki/common/DisplayHost.kt @@ -20,8 +20,8 @@ class DiscoveredDisplayHost( ): DisplayHost() { override val host get() = discoveredHost.hostAddr ?: "" - override val name get() = discoveredHost.hostName ?: registeredHost?.ps4Nickname - override val id get() = discoveredHost.hostId ?: registeredHost?.ps4Mac?.toString() + override val name get() = discoveredHost.hostName ?: registeredHost?.serverNickname + override val id get() = discoveredHost.hostId ?: registeredHost?.serverMac?.toString() override fun equals(other: Any?): Boolean = if(other !is DiscoveredDisplayHost) @@ -40,8 +40,8 @@ class ManualDisplayHost( ): DisplayHost() { override val host get() = manualHost.host - override val name get() = registeredHost?.ps4Nickname - override val id get() = registeredHost?.ps4Mac?.toString() + override val name get() = registeredHost?.serverNickname + override val id get() = registeredHost?.serverMac?.toString() override fun equals(other: Any?): Boolean = if(other !is ManualDisplayHost) diff --git a/android/app/src/main/java/com/metallic/chiaki/common/RegisteredHost.kt b/android/app/src/main/java/com/metallic/chiaki/common/RegisteredHost.kt index 203ff75..b96d88d 100644 --- a/android/app/src/main/java/com/metallic/chiaki/common/RegisteredHost.kt +++ b/android/app/src/main/java/com/metallic/chiaki/common/RegisteredHost.kt @@ -5,6 +5,7 @@ package com.metallic.chiaki.common import androidx.room.* import androidx.room.ColumnInfo.BLOB import com.metallic.chiaki.lib.RegistHost +import com.metallic.chiaki.lib.Target import io.reactivex.Completable import io.reactivex.Flowable import io.reactivex.Maybe @@ -13,24 +14,26 @@ import io.reactivex.Single @Entity(tableName = "registered_host") data class RegisteredHost( @PrimaryKey(autoGenerate = true) val id: Long = 0, + @ColumnInfo(name = "target") val target: Target, @ColumnInfo(name = "ap_ssid") val apSsid: String?, @ColumnInfo(name = "ap_bssid") val apBssid: String?, @ColumnInfo(name = "ap_key") val apKey: String?, @ColumnInfo(name = "ap_name") val apName: String?, - @ColumnInfo(name = "ps4_mac") val ps4Mac: MacAddress, - @ColumnInfo(name = "ps4_nickname") val ps4Nickname: String?, + @ColumnInfo(name = "server_mac") val serverMac: MacAddress, + @ColumnInfo(name = "server_nickname") val serverNickname: String?, @ColumnInfo(name = "rp_regist_key", typeAffinity = BLOB) val rpRegistKey: ByteArray, // CHIAKI_SESSION_AUTH_SIZE @ColumnInfo(name = "rp_key_type") val rpKeyType: Int, @ColumnInfo(name = "rp_key", typeAffinity = BLOB) val rpKey: ByteArray // 0x10 ) { constructor(registHost: RegistHost) : this( + target = registHost.target, apSsid = registHost.apSsid, apBssid = registHost.apBssid, apKey = registHost.apKey, apName = registHost.apName, - ps4Mac = MacAddress(registHost.ps4Mac), - ps4Nickname = registHost.ps4Nickname, + serverMac = MacAddress(registHost.serverMac), + serverNickname = registHost.serverNickname, rpRegistKey = registHost.rpRegistKey, rpKeyType = registHost.rpKeyType.toInt(), rpKey = registHost.rpKey @@ -44,12 +47,13 @@ data class RegisteredHost( other as RegisteredHost if(id != other.id) return false + if(target != other.target) return false if(apSsid != other.apSsid) return false if(apBssid != other.apBssid) return false if(apKey != other.apKey) return false if(apName != other.apName) return false - if(ps4Mac != other.ps4Mac) return false - if(ps4Nickname != other.ps4Nickname) return false + if(serverMac != other.serverMac) return false + if(serverNickname != other.serverNickname) return false if(!rpRegistKey.contentEquals(other.rpRegistKey)) return false if(rpKeyType != other.rpKeyType) return false if(!rpKey.contentEquals(other.rpKey)) return false @@ -60,12 +64,13 @@ data class RegisteredHost( override fun hashCode(): Int { var result = id.hashCode() + result = 31 * result + target.hashCode() result = 31 * result + (apSsid?.hashCode() ?: 0) result = 31 * result + (apBssid?.hashCode() ?: 0) result = 31 * result + (apKey?.hashCode() ?: 0) result = 31 * result + (apName?.hashCode() ?: 0) - result = 31 * result + ps4Mac.hashCode() - result = 31 * result + (ps4Nickname?.hashCode() ?: 0) + result = 31 * result + serverMac.hashCode() + result = 31 * result + (serverNickname?.hashCode() ?: 0) result = 31 * result + rpRegistKey.contentHashCode() result = 31 * result + rpKeyType result = 31 * result + rpKey.contentHashCode() @@ -79,10 +84,10 @@ interface RegisteredHostDao @Query("SELECT * FROM registered_host") fun getAll(): Flowable> - @Query("SELECT * FROM registered_host WHERE ps4_mac == :mac LIMIT 1") + @Query("SELECT * FROM registered_host WHERE server_mac == :mac LIMIT 1") fun getByMac(mac: MacAddress): Maybe - @Query("DELETE FROM registered_host WHERE ps4_mac == :mac") + @Query("DELETE FROM registered_host WHERE server_mac == :mac") fun deleteByMac(mac: MacAddress): Completable @Delete diff --git a/android/app/src/main/java/com/metallic/chiaki/common/SerializedSettings.kt b/android/app/src/main/java/com/metallic/chiaki/common/SerializedSettings.kt index eb6c9e4..7da4109 100644 --- a/android/app/src/main/java/com/metallic/chiaki/common/SerializedSettings.kt +++ b/android/app/src/main/java/com/metallic/chiaki/common/SerializedSettings.kt @@ -12,6 +12,7 @@ import androidx.core.content.FileProvider import androidx.room.* import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.metallic.chiaki.R +import com.metallic.chiaki.lib.Target import com.squareup.moshi.* import io.reactivex.Completable import io.reactivex.Flowable @@ -29,23 +30,25 @@ import java.io.IOException @JsonClass(generateAdapter = true) class SerializedRegisteredHost( + @Json(name = "target") val target: Target, @Json(name = "ap_ssid") val apSsid: String?, @Json(name = "ap_bssid") val apBssid: String?, @Json(name = "ap_key") val apKey: String?, @Json(name = "ap_name") val apName: String?, - @Json(name = "ps4_mac") val ps4Mac: MacAddress, - @Json(name = "ps4_nickname") val ps4Nickname: String?, + @Json(name = "server_mac") val serverMac: MacAddress, + @Json(name = "server_nickname") val serverNickname: String?, @Json(name = "rp_regist_key") val rpRegistKey: ByteArray, @Json(name = "rp_key_type") val rpKeyType: Int, @Json(name = "rp_key") val rpKey: ByteArray ){ constructor(registeredHost: RegisteredHost) : this( + registeredHost.target, registeredHost.apSsid, registeredHost.apBssid, registeredHost.apKey, registeredHost.apName, - registeredHost.ps4Mac, - registeredHost.ps4Nickname, + registeredHost.serverMac, + registeredHost.serverNickname, registeredHost.rpRegistKey, registeredHost.rpKeyType, registeredHost.rpKey @@ -55,7 +58,7 @@ class SerializedRegisteredHost( @JsonClass(generateAdapter = true) class SerializedManualHost( @Json(name = "host") val host: String, - @Json(name = "ps4_mac") val ps4Mac: MacAddress? + @Json(name = "server_mac") val serverMac: MacAddress? ) @JsonClass(generateAdapter = true) @@ -77,7 +80,7 @@ data class SerializedSettings( manualHost.host, manualHost.registeredHost?.let { registeredHostId -> registeredHosts.firstOrNull { it.id == registeredHostId } - }?.ps4Mac + }?.serverMac ) }) } @@ -197,13 +200,13 @@ fun importSettingsFromUri(activity: Activity, uri: Uri, disposable: CompositeDis if(it.isEmpty()) "-" else - it.joinToString(separator = "") { host -> "\n - ${host.ps4Nickname ?: "?"} / ${host.ps4Mac}" } + it.joinToString(separator = "") { host -> "\n - ${host.serverNickname ?: "?"} / ${host.serverMac}" } }, settings.manualHosts.let { if(it.isEmpty()) "-" else - it.joinToString(separator = "") { host -> "\n - ${host.host} / ${host.ps4Mac ?: "unregistered"}" } + it.joinToString(separator = "") { host -> "\n - ${host.host} / ${host.serverMac ?: "unregistered"}" } } )) .setTitle(R.string.alert_title_import) @@ -242,7 +245,7 @@ abstract class ImportDao class IdWithMac(val id: Long, val mac: MacAddress) - @Query("SELECT id, ps4_mac AS mac FROM registered_host WHERE ps4_mac IN (:macs)") + @Query("SELECT id, server_mac AS mac FROM registered_host WHERE server_mac IN (:macs)") abstract fun registeredHostsByMac(macs: List): List @Transaction @@ -251,19 +254,20 @@ abstract class ImportDao insertRegisteredHosts( settings.registeredHosts.map { RegisteredHost( + target = it.target, apSsid = it.apSsid, apBssid = it.apBssid, apKey = it.apKey, apName = it.apName, - ps4Mac = it.ps4Mac, - ps4Nickname = it.ps4Nickname, + serverMac = it.serverMac, + serverNickname = it.serverNickname, rpRegistKey = it.rpRegistKey, rpKeyType = it.rpKeyType, rpKey = it.rpKey ) }) - val macs = settings.manualHosts.mapNotNull { it.ps4Mac } + val macs = settings.manualHosts.mapNotNull { it.serverMac } val idMacs = if(macs.isNotEmpty()) registeredHostsByMac(macs) @@ -274,7 +278,7 @@ abstract class ImportDao settings.manualHosts.map { ManualHost( host = it.host, - registeredHost = idMacs.firstOrNull { regHost -> regHost.mac == it.ps4Mac }?.id + registeredHost = idMacs.firstOrNull { regHost -> regHost.mac == it.serverMac }?.id ) }) } diff --git a/android/app/src/main/java/com/metallic/chiaki/discovery/DiscoveryManager.kt b/android/app/src/main/java/com/metallic/chiaki/discovery/DiscoveryManager.kt index 1579d83..5d23ae5 100644 --- a/android/app/src/main/java/com/metallic/chiaki/discovery/DiscoveryManager.kt +++ b/android/app/src/main/java/com/metallic/chiaki/discovery/DiscoveryManager.kt @@ -92,14 +92,14 @@ class DiscoveryManager disposable.dispose() } - fun sendWakeup(host: String, registKey: ByteArray) + fun sendWakeup(host: String, registKey: ByteArray, ps5: Boolean) { val registKeyString = registKey.indexOfFirst { it == 0.toByte() }.let { end -> registKey.copyOfRange(0, if(end >= 0) end else registKey.size) }.toString(StandardCharsets.UTF_8) val credential = try { registKeyString.toULong(16) } catch(e: NumberFormatException) { Log.e("DiscoveryManager", "Failed to convert registKey to int", e) return } - DiscoveryService.wakeup(discoveryService, host, credential) + DiscoveryService.wakeup(discoveryService, host, credential, ps5) } private fun updateService() diff --git a/android/app/src/main/java/com/metallic/chiaki/lib/Chiaki.kt b/android/app/src/main/java/com/metallic/chiaki/lib/Chiaki.kt index bda96ac..ac3f100 100644 --- a/android/app/src/main/java/com/metallic/chiaki/lib/Chiaki.kt +++ b/android/app/src/main/java/com/metallic/chiaki/lib/Chiaki.kt @@ -11,9 +11,20 @@ import kotlin.math.abs enum class Target(val value: Int) { + PS4_UNKNOWN(0), PS4_8(800), PS4_9(900), - PS4_10(1000) + PS4_10(1000), + PS5_UNKNOWN(1000000), + PS5_1(1000100); + + companion object + { + @JvmStatic + fun fromValue(value: Int) = values().firstOrNull { it.value == value } ?: PS4_10 + } + + val isPS5 get() = value >= PS5_UNKNOWN.value } enum class VideoResolutionPreset(val value: Int) @@ -76,7 +87,7 @@ private class ChiakiNative @JvmStatic external fun sessionSetLoginPin(ptr: Long, pin: String) @JvmStatic external fun discoveryServiceCreate(result: CreateResult, options: DiscoveryServiceOptions, javaService: DiscoveryService) @JvmStatic external fun discoveryServiceFree(ptr: Long) - @JvmStatic external fun discoveryServiceWakeup(ptr: Long, host: String, userCredential: Long) + @JvmStatic external fun discoveryServiceWakeup(ptr: Long, host: String, userCredential: Long, ps5: Boolean) @JvmStatic external fun registStart(result: CreateResult, registInfo: RegistInfo, javaLog: ChiakiLog, javaRegist: Regist) @JvmStatic external fun registStop(ptr: Long) @JvmStatic external fun registFree(ptr: Long) @@ -289,8 +300,8 @@ class DiscoveryService( { companion object { - fun wakeup(service: DiscoveryService?, host: String, userCredential: ULong) = - ChiakiNative.discoveryServiceWakeup(service?.nativePtr ?: 0, host, userCredential.toLong()) + fun wakeup(service: DiscoveryService?, host: String, userCredential: ULong, ps5: Boolean) = + ChiakiNative.discoveryServiceWakeup(service?.nativePtr ?: 0, host, userCredential.toLong(), ps5) } private var nativePtr: Long @@ -339,12 +350,13 @@ data class RegistInfo( } data class RegistHost( + val target: Target, val apSsid: String, val apBssid: String, val apKey: String, val apName: String, - val ps4Mac: ByteArray, - val ps4Nickname: String, + val serverMac: ByteArray, + val serverNickname: String, val rpRegistKey: ByteArray, val rpKeyType: UInt, val rpKey: ByteArray diff --git a/android/app/src/main/java/com/metallic/chiaki/main/MainActivity.kt b/android/app/src/main/java/com/metallic/chiaki/main/MainActivity.kt index 779c789..975fd2f 100644 --- a/android/app/src/main/java/com/metallic/chiaki/main/MainActivity.kt +++ b/android/app/src/main/java/com/metallic/chiaki/main/MainActivity.kt @@ -209,7 +209,7 @@ class MainActivity : AppCompatActivity() private fun wakeupHost(host: DisplayHost) { val registeredHost = host.registeredHost ?: return - viewModel.discoveryManager.sendWakeup(host.host, registeredHost.rpRegistKey) + viewModel.discoveryManager.sendWakeup(host.host, registeredHost.rpRegistKey, registeredHost.target.isPS5) } private fun editHost(host: DisplayHost) diff --git a/android/app/src/main/java/com/metallic/chiaki/main/MainViewModel.kt b/android/app/src/main/java/com/metallic/chiaki/main/MainViewModel.kt index 6224be5..0ed203a 100644 --- a/android/app/src/main/java/com/metallic/chiaki/main/MainViewModel.kt +++ b/android/app/src/main/java/com/metallic/chiaki/main/MainViewModel.kt @@ -31,7 +31,7 @@ class MainViewModel(val database: AppDatabase, val preferences: Preferences): Vi database.registeredHostDao().getAll().toObservable(), discoveryManager.discoveredHosts) { manualHosts, registeredHosts, discoveredHosts -> - val macRegisteredHosts = registeredHosts.associateBy { it.ps4Mac } + val macRegisteredHosts = registeredHosts.associateBy { it.serverMac } val idRegisteredHosts = registeredHosts.associateBy { it.id } discoveredHosts.map { DiscoveredDisplayHost(it.ps4Mac?.let { mac -> macRegisteredHosts[mac] }, it) diff --git a/android/app/src/main/java/com/metallic/chiaki/manualconsole/EditManualConsoleActivity.kt b/android/app/src/main/java/com/metallic/chiaki/manualconsole/EditManualConsoleActivity.kt index d61fe30..49e5f4d 100644 --- a/android/app/src/main/java/com/metallic/chiaki/manualconsole/EditManualConsoleActivity.kt +++ b/android/app/src/main/java/com/metallic/chiaki/manualconsole/EditManualConsoleActivity.kt @@ -81,7 +81,7 @@ class EditManualConsoleActivity: AppCompatActivity(), RevealActivity if(registeredHost == null) getString(R.string.add_manual_regist_on_connect) else - "${registeredHost.ps4Nickname ?: ""} (${registeredHost.ps4Mac})" + "${registeredHost.serverNickname ?: ""} (${registeredHost.serverMac})" private fun saveHost() { diff --git a/android/app/src/main/java/com/metallic/chiaki/regist/RegistExecuteActivity.kt b/android/app/src/main/java/com/metallic/chiaki/regist/RegistExecuteActivity.kt index 88e0f6b..870b67e 100644 --- a/android/app/src/main/java/com/metallic/chiaki/regist/RegistExecuteActivity.kt +++ b/android/app/src/main/java/com/metallic/chiaki/regist/RegistExecuteActivity.kt @@ -114,7 +114,7 @@ class RegistExecuteActivity: AppCompatActivity() if(dialog != null) return - val macStr = viewModel.host?.ps4Mac?.let { MacAddress(it).toString() } ?: "" + val macStr = viewModel.host?.serverMac?.let { MacAddress(it).toString() } ?: "" dialog = MaterialAlertDialogBuilder(this) .setMessage(getString(R.string.alert_regist_duplicate, macStr)) diff --git a/android/app/src/main/java/com/metallic/chiaki/regist/RegistExecuteViewModel.kt b/android/app/src/main/java/com/metallic/chiaki/regist/RegistExecuteViewModel.kt index 9d8a0d5..822fb8e 100644 --- a/android/app/src/main/java/com/metallic/chiaki/regist/RegistExecuteViewModel.kt +++ b/android/app/src/main/java/com/metallic/chiaki/regist/RegistExecuteViewModel.kt @@ -78,7 +78,7 @@ class RegistExecuteViewModel(val database: AppDatabase): ViewModel() private fun registSuccess(host: RegistHost) { this.host = host - database.registeredHostDao().getByMac(MacAddress(host.ps4Mac)) + database.registeredHostDao().getByMac(MacAddress(host.serverMac)) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .doOnSuccess { @@ -98,7 +98,7 @@ class RegistExecuteViewModel(val database: AppDatabase): ViewModel() val dao = database.registeredHostDao() val manualHostDao = database.manualHostDao() val registeredHost = RegisteredHost(host) - dao.deleteByMac(registeredHost.ps4Mac) + dao.deleteByMac(registeredHost.serverMac) .andThen(dao.insert(registeredHost)) .let { if(assignManualHostId != null) diff --git a/android/app/src/main/java/com/metallic/chiaki/settings/SettingsRegisteredHostsAdapter.kt b/android/app/src/main/java/com/metallic/chiaki/settings/SettingsRegisteredHostsAdapter.kt index b92d7f7..84eebe5 100644 --- a/android/app/src/main/java/com/metallic/chiaki/settings/SettingsRegisteredHostsAdapter.kt +++ b/android/app/src/main/java/com/metallic/chiaki/settings/SettingsRegisteredHostsAdapter.kt @@ -29,7 +29,7 @@ class SettingsRegisteredHostsAdapter: RecyclerView.Adapter viewModel.deleteHost(host) } From 5ef9983f959894c02fab34a2793ae4d5142c026d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Mon, 28 Dec 2020 22:30:11 +0100 Subject: [PATCH 121/237] Finish PS5 on Android --- android/app/src/main/cpp/chiaki-jni.c | 37 ++++++++++++------ android/app/src/main/cpp/video-decoder.c | 21 +++++++++- android/app/src/main/cpp/video-decoder.h | 4 +- .../com/metallic/chiaki/common/DisplayHost.kt | 3 ++ .../com/metallic/chiaki/common/Preferences.kt | 20 +++++++++- .../chiaki/common/SerializedSettings.kt | 2 +- .../chiaki/discovery/DiscoveryManager.kt | 2 +- .../java/com/metallic/chiaki/lib/Chiaki.kt | 19 +++++++-- .../main/DisplayHostRecyclerViewAdapter.kt | 17 ++++---- .../com/metallic/chiaki/main/MainActivity.kt | 2 +- .../com/metallic/chiaki/main/MainViewModel.kt | 4 +- .../metallic/chiaki/regist/RegistActivity.kt | 39 ++++++++++--------- .../metallic/chiaki/regist/RegistViewModel.kt | 11 +++--- .../chiaki/settings/SettingsFragment.kt | 12 ++++++ .../app/src/main/res/drawable/ic_codec.xml | 15 +++++++ .../src/main/res/drawable/ic_console_ps5.xml | 9 +++++ .../res/drawable/ic_console_ps5_ready.xml | 16 ++++++++ .../res/drawable/ic_console_ps5_standby.xml | 16 ++++++++ .../src/main/res/layout/activity_regist.xml | 12 +++++- .../src/main/res/layout/item_display_host.xml | 7 ++-- android/app/src/main/res/values/strings.xml | 5 +++ android/app/src/main/res/xml/preferences.xml | 6 +++ 22 files changed, 219 insertions(+), 60 deletions(-) create mode 100644 android/app/src/main/res/drawable/ic_codec.xml create mode 100644 android/app/src/main/res/drawable/ic_console_ps5.xml create mode 100644 android/app/src/main/res/drawable/ic_console_ps5_ready.xml create mode 100644 android/app/src/main/res/drawable/ic_console_ps5_standby.xml diff --git a/android/app/src/main/cpp/chiaki-jni.c b/android/app/src/main/cpp/chiaki-jni.c index a65c2e3..35543ad 100644 --- a/android/app/src/main/cpp/chiaki-jni.c +++ b/android/app/src/main/cpp/chiaki-jni.c @@ -115,13 +115,13 @@ JNIEXPORT jboolean JNICALL JNI_FCN(quitReasonIsStopped)(JNIEnv *env, jobject obj return value == CHIAKI_QUIT_REASON_STOPPED; } -JNIEXPORT jobject JNICALL JNI_FCN(videoProfilePreset)(JNIEnv *env, jobject obj, jint resolution_preset, jint fps_preset) +JNIEXPORT jobject JNICALL JNI_FCN(videoProfilePreset)(JNIEnv *env, jobject obj, jint resolution_preset, jint fps_preset, jobject codec) { ChiakiConnectVideoProfile profile = { 0 }; chiaki_connect_video_profile_preset(&profile, (ChiakiVideoResolutionPreset)resolution_preset, (ChiakiVideoFPSPreset)fps_preset); jclass profile_class = E->FindClass(env, BASE_PACKAGE"/ConnectVideoProfile"); - jmethodID profile_ctor = E->GetMethodID(env, profile_class, "", "(IIII)V"); - return E->NewObject(env, profile_class, profile_ctor, profile.width, profile.height, profile.max_fps, profile.bitrate); + jmethodID profile_ctor = E->GetMethodID(env, profile_class, "", "(IIIIL"BASE_PACKAGE"/Codec;)V"); + return E->NewObject(env, profile_class, profile_ctor, profile.width, profile.height, profile.max_fps, profile.bitrate, codec); } typedef struct android_chiaki_session_t @@ -198,6 +198,7 @@ JNIEXPORT void JNICALL JNI_FCN(sessionCreate)(JNIEnv *env, jobject obj, jobject jclass result_class = E->GetObjectClass(env, result); jclass connect_info_class = E->GetObjectClass(env, connect_info_obj); + jboolean ps5 = E->GetBooleanField(env, connect_info_obj, E->GetFieldID(env, connect_info_class, "ps5", "Z")); jstring host_string = E->GetObjectField(env, connect_info_obj, E->GetFieldID(env, connect_info_class, "host", "Ljava/lang/String;")); jbyteArray regist_key_array = E->GetObjectField(env, connect_info_obj, E->GetFieldID(env, connect_info_class, "registKey", "[B")); jbyteArray morning_array = E->GetObjectField(env, connect_info_obj, E->GetFieldID(env, connect_info_class, "morning", "[B")); @@ -205,6 +206,8 @@ JNIEXPORT void JNICALL JNI_FCN(sessionCreate)(JNIEnv *env, jobject obj, jobject jclass connect_video_profile_class = E->GetObjectClass(env, connect_video_profile_obj); ChiakiConnectInfo connect_info = { 0 }; + connect_info.ps5 = ps5; + const char *str_borrow = E->GetStringUTFChars(env, host_string, NULL); connect_info.host = host_str = strdup(str_borrow); E->ReleaseStringUTFChars(env, host_string, str_borrow); @@ -239,6 +242,13 @@ JNIEXPORT void JNICALL JNI_FCN(sessionCreate)(JNIEnv *env, jobject obj, jobject connect_info.video_profile.max_fps = (unsigned int)E->GetIntField(env, connect_video_profile_obj, E->GetFieldID(env, connect_video_profile_class, "maxFPS", "I")); connect_info.video_profile.bitrate = (unsigned int)E->GetIntField(env, connect_video_profile_obj, E->GetFieldID(env, connect_video_profile_class, "bitrate", "I")); + jobject codec_obj = E->GetObjectField(env, connect_video_profile_obj, E->GetFieldID(env, connect_video_profile_class, "codec", "L"BASE_PACKAGE"/Codec;")); + jclass codec_class = E->GetObjectClass(env, codec_obj); + jint target_value = E->GetIntField(env, codec_obj, E->GetFieldID(env, codec_class, "value", "I")); + connect_info.video_profile.codec = (ChiakiCodec)target_value; + + connect_info.video_profile_auto_downgrade = true; + session = CHIAKI_NEW(AndroidChiakiSession); if(!session) { @@ -247,7 +257,8 @@ JNIEXPORT void JNICALL JNI_FCN(sessionCreate)(JNIEnv *env, jobject obj, jobject } memset(session, 0, sizeof(AndroidChiakiSession)); session->log = log; - err = android_chiaki_video_decoder_init(&session->video_decoder, log, connect_info.video_profile.width, connect_info.video_profile.height); + err = android_chiaki_video_decoder_init(&session->video_decoder, log, connect_info.video_profile.width, connect_info.video_profile.height, + connect_info.ps5 ? connect_info.video_profile.codec : CHIAKI_CODEC_H264); if(err != CHIAKI_ERR_SUCCESS) { free(session); @@ -592,6 +603,8 @@ typedef struct android_chiaki_regist_t jobject java_regist; jmethodID java_regist_event_meth; + jclass java_target_class; + jobject java_regist_event_canceled; jobject java_regist_event_failed; jclass java_regist_event_success_class; @@ -601,11 +614,10 @@ typedef struct android_chiaki_regist_t jmethodID java_regist_host_ctor; } AndroidChiakiRegist; -static jobject create_jni_target(JNIEnv *env, ChiakiTarget target) +static jobject create_jni_target(JNIEnv *env, jclass target_class, ChiakiTarget target) { - jclass cls = E->FindClass(env, BASE_PACKAGE"/Target"); - jmethodID meth = E->GetStaticMethodID(env, cls, "fromValue", "(I)L"BASE_PACKAGE"/Target;"); - return E->CallStaticObjectMethod(env, cls, meth, (jint)target); + jmethodID meth = E->GetStaticMethodID(env, target_class, "fromValue", "(I)L"BASE_PACKAGE"/Target;"); + return E->CallStaticObjectMethod(env, target_class, meth, (jint)target); } static void android_chiaki_regist_cb(ChiakiRegistEvent *event, void *user) @@ -629,7 +641,7 @@ static void android_chiaki_regist_cb(ChiakiRegistEvent *event, void *user) { ChiakiRegisteredHost *host = event->registered_host; jobject java_host = E->NewObject(env, regist->java_regist_host_class, regist->java_regist_host_ctor, - create_jni_target(env, host->target), + create_jni_target(env, regist->java_target_class, host->target), jnistr_from_ascii(env, host->ap_ssid), jnistr_from_ascii(env, host->ap_bssid), jnistr_from_ascii(env, host->ap_key), @@ -654,6 +666,7 @@ static void android_chiaki_regist_fini_partial(JNIEnv *env, AndroidChiakiRegist { android_chiaki_jni_log_fini(®ist->log, env); E->DeleteGlobalRef(env, regist->java_regist); + E->DeleteGlobalRef(env, regist->java_target_class); E->DeleteGlobalRef(env, regist->java_regist_event_canceled); E->DeleteGlobalRef(env, regist->java_regist_event_failed); E->DeleteGlobalRef(env, regist->java_regist_event_success_class); @@ -676,6 +689,8 @@ JNIEXPORT void JNICALL JNI_FCN(registStart)(JNIEnv *env, jobject obj, jobject re regist->java_regist = E->NewGlobalRef(env, java_regist); regist->java_regist_event_meth = E->GetMethodID(env, E->GetObjectClass(env, regist->java_regist), "event", "(L"BASE_PACKAGE"/RegistEvent;)V"); + regist->java_target_class = E->NewGlobalRef(env, E->FindClass(env, BASE_PACKAGE"/Target")); + regist->java_regist_event_canceled = E->NewGlobalRef(env, get_kotlin_global_object(env, BASE_PACKAGE"/RegistEventCanceled")); regist->java_regist_event_failed = E->NewGlobalRef(env, get_kotlin_global_object(env, BASE_PACKAGE"/RegistEventFailed")); regist->java_regist_event_success_class = E->NewGlobalRef(env, E->FindClass(env, BASE_PACKAGE"/RegistEventSuccess")); @@ -688,8 +703,8 @@ JNIEXPORT void JNICALL JNI_FCN(registStart)(JNIEnv *env, jobject obj, jobject re "Ljava/lang/String;" // apBssid: String "Ljava/lang/String;" // apKey: String "Ljava/lang/String;" // apName: String - "[B" // ps4Mac: ByteArray - "Ljava/lang/String;" // ps4Nickname: String + "[B" // serverMac: ByteArray + "Ljava/lang/String;" // serverNickname: String "[B" // rpRegistKey: ByteArray "I" // rpKeyType: UInt "[B" // rpKey: ByteArray diff --git a/android/app/src/main/cpp/video-decoder.c b/android/app/src/main/cpp/video-decoder.c index 43c9bc4..d57623d 100644 --- a/android/app/src/main/cpp/video-decoder.c +++ b/android/app/src/main/cpp/video-decoder.c @@ -14,13 +14,15 @@ static void *android_chiaki_video_decoder_output_thread_func(void *user); -ChiakiErrorCode android_chiaki_video_decoder_init(AndroidChiakiVideoDecoder *decoder, ChiakiLog *log, int32_t target_width, int32_t target_height) +ChiakiErrorCode android_chiaki_video_decoder_init(AndroidChiakiVideoDecoder *decoder, ChiakiLog *log, int32_t target_width, int32_t target_height, ChiakiCodec codec) { decoder->log = log; decoder->codec = NULL; decoder->timestamp_cur = 0; decoder->target_width = target_width; decoder->target_height = target_height; + decoder->target_codec = codec; + decoder->shutdown_output = false; return chiaki_mutex_init(&decoder->codec_mutex, false); } @@ -29,17 +31,20 @@ void android_chiaki_video_decoder_fini(AndroidChiakiVideoDecoder *decoder) if(decoder->codec) { chiaki_mutex_lock(&decoder->codec_mutex); + decoder->shutdown_output = true; ssize_t codec_buf_index = AMediaCodec_dequeueInputBuffer(decoder->codec, -1); if(codec_buf_index >= 0) { CHIAKI_LOGI(decoder->log, "Video Decoder sending EOS buffer"); AMediaCodec_queueInputBuffer(decoder->codec, (size_t)codec_buf_index, 0, 0, decoder->timestamp_cur++, AMEDIACODEC_BUFFER_FLAG_END_OF_STREAM); + AMediaCodec_stop(decoder->codec); chiaki_mutex_unlock(&decoder->codec_mutex); chiaki_thread_join(&decoder->output_thread, NULL); } else { CHIAKI_LOGE(decoder->log, "Failed to get input buffer for shutting down Video Decoder!"); + AMediaCodec_stop(decoder->codec); chiaki_mutex_unlock(&decoder->codec_mutex); } AMediaCodec_delete(decoder->codec); @@ -67,7 +72,8 @@ void android_chiaki_video_decoder_set_surface(AndroidChiakiVideoDecoder *decoder decoder->window = ANativeWindow_fromSurface(env, surface); - const char *mime = "video/avc"; + const char *mime = chiaki_codec_is_h265(decoder->target_codec) ? "video/hevc" : "video/avc"; + CHIAKI_LOGI(decoder->log, "Initializing decoder with mime %s", mime); decoder->codec = AMediaCodec_createDecoderByType(mime); if(!decoder->codec) @@ -181,6 +187,17 @@ static void *android_chiaki_video_decoder_output_thread_func(void *user) break; } } + else + { + chiaki_mutex_lock(&decoder->codec_mutex); + bool shutdown = decoder->shutdown_output; + chiaki_mutex_unlock(&decoder->codec_mutex); + if(shutdown) + { + CHIAKI_LOGI(decoder->log, "Video Decoder Output Thread detected shutdown after reported error"); + break; + } + } } CHIAKI_LOGI(decoder->log, "Video Decoder Output Thread exiting"); diff --git a/android/app/src/main/cpp/video-decoder.h b/android/app/src/main/cpp/video-decoder.h index 0578e7f..b12ba85 100644 --- a/android/app/src/main/cpp/video-decoder.h +++ b/android/app/src/main/cpp/video-decoder.h @@ -19,11 +19,13 @@ typedef struct android_chiaki_video_decoder_t ANativeWindow *window; uint64_t timestamp_cur; ChiakiThread output_thread; + bool shutdown_output; int32_t target_width; int32_t target_height; + ChiakiCodec target_codec; } AndroidChiakiVideoDecoder; -ChiakiErrorCode android_chiaki_video_decoder_init(AndroidChiakiVideoDecoder *decoder, ChiakiLog *log, int32_t target_width, int32_t target_height); +ChiakiErrorCode android_chiaki_video_decoder_init(AndroidChiakiVideoDecoder *decoder, ChiakiLog *log, int32_t target_width, int32_t target_height, ChiakiCodec codec); void android_chiaki_video_decoder_fini(AndroidChiakiVideoDecoder *decoder); void android_chiaki_video_decoder_set_surface(AndroidChiakiVideoDecoder *decoder, JNIEnv *env, jobject surface); bool android_chiaki_video_decoder_video_sample(uint8_t *buf, size_t buf_size, void *user); diff --git a/android/app/src/main/java/com/metallic/chiaki/common/DisplayHost.kt b/android/app/src/main/java/com/metallic/chiaki/common/DisplayHost.kt index b7c22eb..a25f0df 100644 --- a/android/app/src/main/java/com/metallic/chiaki/common/DisplayHost.kt +++ b/android/app/src/main/java/com/metallic/chiaki/common/DisplayHost.kt @@ -10,6 +10,7 @@ sealed class DisplayHost abstract val host: String abstract val name: String? abstract val id: String? + abstract val isPS5: Boolean val isRegistered get() = registeredHost != null } @@ -22,6 +23,7 @@ class DiscoveredDisplayHost( override val host get() = discoveredHost.hostAddr ?: "" override val name get() = discoveredHost.hostName ?: registeredHost?.serverNickname override val id get() = discoveredHost.hostId ?: registeredHost?.serverMac?.toString() + override val isPS5 get() = discoveredHost.isPS5 override fun equals(other: Any?): Boolean = if(other !is DiscoveredDisplayHost) @@ -42,6 +44,7 @@ class ManualDisplayHost( override val host get() = manualHost.host override val name get() = registeredHost?.serverNickname override val id get() = registeredHost?.serverMac?.toString() + override val isPS5: Boolean get() = registeredHost?.target?.isPS5 ?: false override fun equals(other: Any?): Boolean = if(other !is ManualDisplayHost) diff --git a/android/app/src/main/java/com/metallic/chiaki/common/Preferences.kt b/android/app/src/main/java/com/metallic/chiaki/common/Preferences.kt index 3da0a22..c9691b5 100644 --- a/android/app/src/main/java/com/metallic/chiaki/common/Preferences.kt +++ b/android/app/src/main/java/com/metallic/chiaki/common/Preferences.kt @@ -7,6 +7,7 @@ import android.content.SharedPreferences import androidx.annotation.StringRes import androidx.preference.PreferenceManager import com.metallic.chiaki.R +import com.metallic.chiaki.lib.Codec import com.metallic.chiaki.lib.ConnectVideoProfile import com.metallic.chiaki.lib.VideoFPSPreset import com.metallic.chiaki.lib.VideoResolutionPreset @@ -31,12 +32,20 @@ class Preferences(context: Context) FPS_60("60", R.string.preferences_fps_title_60, VideoFPSPreset.FPS_60) } + enum class Codec(val value: String, @StringRes val title: Int, val codec: com.metallic.chiaki.lib.Codec) + { + CODEC_H264("h264", R.string.preferences_codec_title_h264, com.metallic.chiaki.lib.Codec.CODEC_H264), + CODEC_H265("h265", R.string.preferences_codec_title_h265, com.metallic.chiaki.lib.Codec.CODEC_H265) + } + companion object { val resolutionDefault = Resolution.RES_720P val resolutionAll = Resolution.values() val fpsDefault = FPS.FPS_60 val fpsAll = FPS.values() + val codecDefault = Codec.CODEC_H265 + val codecAll = Codec.values() } private val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context) @@ -97,12 +106,19 @@ class Preferences(context: Context) private val bitrateAutoSubject by lazy { BehaviorSubject.createDefault(bitrateAuto) } val bitrateAutoObservable: Observable get() = bitrateAutoSubject - private val videoProfileDefaultBitrate get() = ConnectVideoProfile.preset(resolution.preset, fps.preset) + val codecKey get() = resources.getString(R.string.preferences_codec_key) + var codec + get() = sharedPreferences.getString(codecKey, codecDefault.value)?.let { value -> + Codec.values().firstOrNull { it.value == value } + } ?: codecDefault + set(value) { sharedPreferences.edit().putString(codecKey, value.value).apply() } + + private val videoProfileDefaultBitrate get() = ConnectVideoProfile.preset(resolution.preset, fps.preset, codec.codec) val videoProfile get() = videoProfileDefaultBitrate.let { val bitrate = bitrate if(bitrate == null) it else - ConnectVideoProfile(it.width, it.height, it.maxFPS, bitrate) + it.copy(bitrate = bitrate) } } \ No newline at end of file diff --git a/android/app/src/main/java/com/metallic/chiaki/common/SerializedSettings.kt b/android/app/src/main/java/com/metallic/chiaki/common/SerializedSettings.kt index 7da4109..8334dc5 100644 --- a/android/app/src/main/java/com/metallic/chiaki/common/SerializedSettings.kt +++ b/android/app/src/main/java/com/metallic/chiaki/common/SerializedSettings.kt @@ -106,7 +106,7 @@ private fun Moshi.serializedSettingsAdapter() = private const val KEY_FORMAT = "format" private const val FORMAT = "chiaki-settings" private const val KEY_VERSION = "version" -private const val VERSION = 1 +private const val VERSION = 2 private const val KEY_SETTINGS = "settings" fun exportAllSettings(db: AppDatabase) = SerializedSettings.fromDatabase(db) diff --git a/android/app/src/main/java/com/metallic/chiaki/discovery/DiscoveryManager.kt b/android/app/src/main/java/com/metallic/chiaki/discovery/DiscoveryManager.kt index 5d23ae5..e7b2e3e 100644 --- a/android/app/src/main/java/com/metallic/chiaki/discovery/DiscoveryManager.kt +++ b/android/app/src/main/java/com/metallic/chiaki/discovery/DiscoveryManager.kt @@ -21,7 +21,7 @@ import java.nio.charset.Charset import java.nio.charset.StandardCharsets import java.util.concurrent.TimeUnit -val DiscoveryHost.ps4Mac get() = this.hostId?.hexToByteArray()?.let { +val DiscoveryHost.serverMac get() = this.hostId?.hexToByteArray()?.let { if(it.size == MacAddress.LENGTH) MacAddress(it) else diff --git a/android/app/src/main/java/com/metallic/chiaki/lib/Chiaki.kt b/android/app/src/main/java/com/metallic/chiaki/lib/Chiaki.kt index ac3f100..d638cb2 100644 --- a/android/app/src/main/java/com/metallic/chiaki/lib/Chiaki.kt +++ b/android/app/src/main/java/com/metallic/chiaki/lib/Chiaki.kt @@ -41,23 +41,32 @@ enum class VideoFPSPreset(val value: Int) FPS_60(60) } +enum class Codec(val value: Int) +{ + CODEC_H264(0), + CODEC_H265(1), + CODEC_H265_HDR(2) +} + @Parcelize data class ConnectVideoProfile( val width: Int, val height: Int, val maxFPS: Int, - val bitrate: Int + val bitrate: Int, + val codec: Codec ): Parcelable { companion object { - fun preset(resolutionPreset: VideoResolutionPreset, fpsPreset: VideoFPSPreset) - = ChiakiNative.videoProfilePreset(resolutionPreset.value, fpsPreset.value) + fun preset(resolutionPreset: VideoResolutionPreset, fpsPreset: VideoFPSPreset, codec: Codec) + = ChiakiNative.videoProfilePreset(resolutionPreset.value, fpsPreset.value, codec) } } @Parcelize data class ConnectInfo( + val ps5: Boolean, val host: String, val registKey: ByteArray, val morning: ByteArray, @@ -76,7 +85,7 @@ private class ChiakiNative @JvmStatic external fun errorCodeToString(value: Int): String @JvmStatic external fun quitReasonToString(value: Int): String @JvmStatic external fun quitReasonIsStopped(value: Int): Boolean - @JvmStatic external fun videoProfilePreset(resolutionPreset: Int, fpsPreset: Int): ConnectVideoProfile + @JvmStatic external fun videoProfilePreset(resolutionPreset: Int, fpsPreset: Int, codec: Codec): ConnectVideoProfile @JvmStatic external fun sessionCreate(result: CreateResult, connectInfo: ConnectInfo, logFile: String?, logVerbose: Boolean, javaSession: Session) @JvmStatic external fun sessionFree(ptr: Long) @JvmStatic external fun sessionStart(ptr: Long): Int @@ -284,6 +293,8 @@ data class DiscoveryHost( READY, STANDBY } + + val isPS5 get() = deviceDiscoveryProtocolVersion == "00030010" } diff --git a/android/app/src/main/java/com/metallic/chiaki/main/DisplayHostRecyclerViewAdapter.kt b/android/app/src/main/java/com/metallic/chiaki/main/DisplayHostRecyclerViewAdapter.kt index 4dd9a69..0bd844e 100644 --- a/android/app/src/main/java/com/metallic/chiaki/main/DisplayHostRecyclerViewAdapter.kt +++ b/android/app/src/main/java/com/metallic/chiaki/main/DisplayHostRecyclerViewAdapter.kt @@ -75,15 +75,18 @@ class DisplayHostRecyclerViewAdapter( } ?: "" it.discoveredIndicatorLayout.visibility = if(host is DiscoveredDisplayHost) View.VISIBLE else View.GONE it.stateIndicatorImageView.setImageResource( - if(host is DiscoveredDisplayHost) - when(host.discoveredHost.state) + when + { + host is DiscoveredDisplayHost -> when(host.discoveredHost.state) { - DiscoveryHost.State.STANDBY -> R.drawable.ic_console_standby - DiscoveryHost.State.READY -> R.drawable.ic_console_ready - else -> R.drawable.ic_console + DiscoveryHost.State.STANDBY -> if(host.isPS5) R.drawable.ic_console_ps5_standby else R.drawable.ic_console_standby + DiscoveryHost.State.READY -> if(host.isPS5) R.drawable.ic_console_ps5_ready else R.drawable.ic_console_ready + else -> if(host.isPS5) R.drawable.ic_console_ps5 else R.drawable.ic_console } - else - R.drawable.ic_console) + host.isPS5 -> R.drawable.ic_console_ps5 + else -> R.drawable.ic_console + } + ) it.setOnClickListener { clickCallback(host) } val canWakeup = host.registeredHost != null diff --git a/android/app/src/main/java/com/metallic/chiaki/main/MainActivity.kt b/android/app/src/main/java/com/metallic/chiaki/main/MainActivity.kt index 975fd2f..4fd8178 100644 --- a/android/app/src/main/java/com/metallic/chiaki/main/MainActivity.kt +++ b/android/app/src/main/java/com/metallic/chiaki/main/MainActivity.kt @@ -170,7 +170,7 @@ class MainActivity : AppCompatActivity() if(registeredHost != null) { fun connect() { - val connectInfo = ConnectInfo(host.host, registeredHost.rpRegistKey, registeredHost.rpKey, Preferences(this).videoProfile) + val connectInfo = ConnectInfo(host.isPS5, host.host, registeredHost.rpRegistKey, registeredHost.rpKey, Preferences(this).videoProfile) Intent(this, StreamActivity::class.java).let { it.putExtra(StreamActivity.EXTRA_CONNECT_INFO, connectInfo) startActivity(it) diff --git a/android/app/src/main/java/com/metallic/chiaki/main/MainViewModel.kt b/android/app/src/main/java/com/metallic/chiaki/main/MainViewModel.kt index 0ed203a..b4fe9c8 100644 --- a/android/app/src/main/java/com/metallic/chiaki/main/MainViewModel.kt +++ b/android/app/src/main/java/com/metallic/chiaki/main/MainViewModel.kt @@ -6,7 +6,7 @@ import androidx.lifecycle.ViewModel import com.metallic.chiaki.common.* import com.metallic.chiaki.common.ext.toLiveData import com.metallic.chiaki.discovery.DiscoveryManager -import com.metallic.chiaki.discovery.ps4Mac +import com.metallic.chiaki.discovery.serverMac import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.disposables.CompositeDisposable import io.reactivex.rxkotlin.Observables @@ -34,7 +34,7 @@ class MainViewModel(val database: AppDatabase, val preferences: Preferences): Vi val macRegisteredHosts = registeredHosts.associateBy { it.serverMac } val idRegisteredHosts = registeredHosts.associateBy { it.id } discoveredHosts.map { - DiscoveredDisplayHost(it.ps4Mac?.let { mac -> macRegisteredHosts[mac] }, it) + DiscoveredDisplayHost(it.serverMac?.let { mac -> macRegisteredHosts[mac] }, it) } + manualHosts.map { ManualDisplayHost(it.registeredHost?.let { id -> idRegisteredHosts[id] }, it) diff --git a/android/app/src/main/java/com/metallic/chiaki/regist/RegistActivity.kt b/android/app/src/main/java/com/metallic/chiaki/regist/RegistActivity.kt index 4f5af54..1332fdc 100644 --- a/android/app/src/main/java/com/metallic/chiaki/regist/RegistActivity.kt +++ b/android/app/src/main/java/com/metallic/chiaki/regist/RegistActivity.kt @@ -49,27 +49,29 @@ class RegistActivity: AppCompatActivity(), RevealActivity registButton.setOnClickListener { doRegist() } - ps4VersionRadioGroup.check(when(viewModel.ps4Version.value ?: RegistViewModel.PS4Version.GE_8) { - RegistViewModel.PS4Version.GE_8 -> R.id.ps4VersionGE8RadioButton - RegistViewModel.PS4Version.GE_7 -> R.id.ps4VersionGE7RadioButton - RegistViewModel.PS4Version.LT_7 -> R.id.ps4VersionLT7RadioButton + ps4VersionRadioGroup.check(when(viewModel.ps4Version.value ?: RegistViewModel.ConsoleVersion.PS5) { + RegistViewModel.ConsoleVersion.PS5 -> R.id.ps5RadioButton + RegistViewModel.ConsoleVersion.PS4_GE_8 -> R.id.ps4VersionGE8RadioButton + RegistViewModel.ConsoleVersion.PS4_GE_7 -> R.id.ps4VersionGE7RadioButton + RegistViewModel.ConsoleVersion.PS4_LT_7 -> R.id.ps4VersionLT7RadioButton }) ps4VersionRadioGroup.setOnCheckedChangeListener { _, checkedId -> viewModel.ps4Version.value = when(checkedId) { - R.id.ps4VersionGE8RadioButton -> RegistViewModel.PS4Version.GE_8 - R.id.ps4VersionGE7RadioButton -> RegistViewModel.PS4Version.GE_7 - R.id.ps4VersionLT7RadioButton -> RegistViewModel.PS4Version.LT_7 - else -> RegistViewModel.PS4Version.GE_7 + R.id.ps5RadioButton -> RegistViewModel.ConsoleVersion.PS5 + R.id.ps4VersionGE8RadioButton -> RegistViewModel.ConsoleVersion.PS4_GE_8 + R.id.ps4VersionGE7RadioButton -> RegistViewModel.ConsoleVersion.PS4_GE_7 + R.id.ps4VersionLT7RadioButton -> RegistViewModel.ConsoleVersion.PS4_LT_7 + else -> RegistViewModel.ConsoleVersion.PS5 } } viewModel.ps4Version.observe(this, Observer { - psnAccountIdHelpGroup.visibility = if(it == RegistViewModel.PS4Version.LT_7) View.GONE else View.VISIBLE + psnAccountIdHelpGroup.visibility = if(it == RegistViewModel.ConsoleVersion.PS4_LT_7) View.GONE else View.VISIBLE psnIdTextInputLayout.hint = getString(when(it!!) { - RegistViewModel.PS4Version.LT_7 -> R.string.hint_regist_psn_online_id + RegistViewModel.ConsoleVersion.PS4_LT_7 -> R.string.hint_regist_psn_online_id else -> R.string.hint_regist_psn_account_id }) }) @@ -77,22 +79,22 @@ class RegistActivity: AppCompatActivity(), RevealActivity private fun doRegist() { - val ps4Version = viewModel.ps4Version.value ?: RegistViewModel.PS4Version.GE_7 + val ps4Version = viewModel.ps4Version.value ?: RegistViewModel.ConsoleVersion.PS5 val host = hostEditText.text.toString().trim() val hostValid = host.isNotEmpty() val broadcast = broadcastCheckBox.isChecked val psnId = psnIdEditText.text.toString().trim() - val psnOnlineId: String? = if(ps4Version == RegistViewModel.PS4Version.LT_7) psnId else null + val psnOnlineId: String? = if(ps4Version == RegistViewModel.ConsoleVersion.PS4_LT_7) psnId else null val psnAccountId: ByteArray? = - if(ps4Version != RegistViewModel.PS4Version.LT_7) + if(ps4Version != RegistViewModel.ConsoleVersion.PS4_LT_7) try { Base64.decode(psnId, Base64.DEFAULT) } catch(e: IllegalArgumentException) { null } else null val psnIdValid = when(ps4Version) { - RegistViewModel.PS4Version.LT_7 -> psnOnlineId?.isNotEmpty() ?: false + RegistViewModel.ConsoleVersion.PS4_LT_7 -> psnOnlineId?.isNotEmpty() ?: false else -> psnAccountId != null && psnAccountId.size == RegistInfo.ACCOUNT_ID_SIZE } @@ -105,7 +107,7 @@ class RegistActivity: AppCompatActivity(), RevealActivity if(!psnIdValid) getString(when(ps4Version) { - RegistViewModel.PS4Version.LT_7 -> R.string.regist_psn_online_id_invalid + RegistViewModel.ConsoleVersion.PS4_LT_7 -> R.string.regist_psn_online_id_invalid else -> R.string.regist_psn_account_id_invalid }) else @@ -117,9 +119,10 @@ class RegistActivity: AppCompatActivity(), RevealActivity val target = when(ps4Version) { - RegistViewModel.PS4Version.GE_8 -> Target.PS4_10 - RegistViewModel.PS4Version.GE_7 -> Target.PS4_9 - RegistViewModel.PS4Version.LT_7 -> Target.PS4_8 + RegistViewModel.ConsoleVersion.PS5 -> Target.PS5_1 + RegistViewModel.ConsoleVersion.PS4_GE_8 -> Target.PS4_10 + RegistViewModel.ConsoleVersion.PS4_GE_7 -> Target.PS4_9 + RegistViewModel.ConsoleVersion.PS4_LT_7 -> Target.PS4_8 } val registInfo = RegistInfo(target, host, broadcast, psnOnlineId, psnAccountId, pin.toInt()) diff --git a/android/app/src/main/java/com/metallic/chiaki/regist/RegistViewModel.kt b/android/app/src/main/java/com/metallic/chiaki/regist/RegistViewModel.kt index d99778e..31c6bda 100644 --- a/android/app/src/main/java/com/metallic/chiaki/regist/RegistViewModel.kt +++ b/android/app/src/main/java/com/metallic/chiaki/regist/RegistViewModel.kt @@ -7,11 +7,12 @@ import androidx.lifecycle.ViewModel class RegistViewModel: ViewModel() { - enum class PS4Version { - GE_8, - GE_7, - LT_7 + enum class ConsoleVersion { + PS5, + PS4_GE_8, + PS4_GE_7, + PS4_LT_7 } - val ps4Version = MutableLiveData(PS4Version.GE_8) + val ps4Version = MutableLiveData(ConsoleVersion.PS5) } \ No newline at end of file diff --git a/android/app/src/main/java/com/metallic/chiaki/settings/SettingsFragment.kt b/android/app/src/main/java/com/metallic/chiaki/settings/SettingsFragment.kt index 2001eed..f38f619 100644 --- a/android/app/src/main/java/com/metallic/chiaki/settings/SettingsFragment.kt +++ b/android/app/src/main/java/com/metallic/chiaki/settings/SettingsFragment.kt @@ -16,6 +16,7 @@ import com.metallic.chiaki.common.exportAndShareAllSettings import com.metallic.chiaki.common.ext.viewModelFactory import com.metallic.chiaki.common.getDatabase import com.metallic.chiaki.common.importSettingsFromUri +import com.metallic.chiaki.lib.Codec import io.reactivex.disposables.CompositeDisposable import io.reactivex.rxkotlin.addTo @@ -42,6 +43,7 @@ class DataStore(val preferences: Preferences): PreferenceDataStore() preferences.resolutionKey -> preferences.resolution.value preferences.fpsKey -> preferences.fps.value preferences.bitrateKey -> preferences.bitrate?.toString() ?: "" + preferences.codecKey -> preferences.codec.value else -> defValue } @@ -60,6 +62,11 @@ class DataStore(val preferences: Preferences): PreferenceDataStore() preferences.fps = fps } preferences.bitrateKey -> preferences.bitrate = value?.toIntOrNull() + preferences.codecKey -> + { + val codec = Preferences.Codec.values().firstOrNull { it.value == value } ?: return + preferences.codec = codec + } } } } @@ -111,6 +118,11 @@ class SettingsFragment: PreferenceFragmentCompat(), TitleFragment bitratePreference?.summaryProvider = bitrateSummaryProvider }) + preferenceScreen.findPreference(getString(R.string.preferences_codec_key))?.let { + it.entryValues = Preferences.codecAll.map { codec -> codec.value }.toTypedArray() + it.entries = Preferences.codecAll.map { codec -> getString(codec.title) }.toTypedArray() + } + val registeredHostsPreference = preferenceScreen.findPreference("registered_hosts") viewModel.registeredHostsCount.observe(this, Observer { registeredHostsPreference?.summary = getString(R.string.preferences_registered_hosts_summary, it) diff --git a/android/app/src/main/res/drawable/ic_codec.xml b/android/app/src/main/res/drawable/ic_codec.xml new file mode 100644 index 0000000..e173d90 --- /dev/null +++ b/android/app/src/main/res/drawable/ic_codec.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/android/app/src/main/res/drawable/ic_console_ps5.xml b/android/app/src/main/res/drawable/ic_console_ps5.xml new file mode 100644 index 0000000..488213b --- /dev/null +++ b/android/app/src/main/res/drawable/ic_console_ps5.xml @@ -0,0 +1,9 @@ + + + diff --git a/android/app/src/main/res/drawable/ic_console_ps5_ready.xml b/android/app/src/main/res/drawable/ic_console_ps5_ready.xml new file mode 100644 index 0000000..d2a3ab0 --- /dev/null +++ b/android/app/src/main/res/drawable/ic_console_ps5_ready.xml @@ -0,0 +1,16 @@ + + + + diff --git a/android/app/src/main/res/drawable/ic_console_ps5_standby.xml b/android/app/src/main/res/drawable/ic_console_ps5_standby.xml new file mode 100644 index 0000000..8d49bbd --- /dev/null +++ b/android/app/src/main/res/drawable/ic_console_ps5_standby.xml @@ -0,0 +1,16 @@ + + + + diff --git a/android/app/src/main/res/layout/activity_regist.xml b/android/app/src/main/res/layout/activity_regist.xml index e174bb0..607fcc8 100644 --- a/android/app/src/main/res/layout/activity_regist.xml +++ b/android/app/src/main/res/layout/activity_regist.xml @@ -66,9 +66,17 @@ android:id="@+id/ps4VersionRadioGroup" android:layout_width="match_parent" android:layout_height="wrap_content" - android:orientation="horizontal" + android:orientation="vertical" app:layout_constraintTop_toBottomOf="@id/broadcastCheckBox"> + + + android:layout_weight="1"/> + android:layout_marginTop="4dp" + android:layout_marginBottom="4dp"/> PS4 < 7.0 PS4 ≥ 7.0, < 8 PS4 ≥ 8.0 + PS5 About obtaining your Account ID, see https://git.sr.ht/~thestr4ng3r/chiaki/tree/master/item/README.md#obtaining-your-psn-accountid PSN Online ID (username, case-sensitive) @@ -71,6 +72,7 @@ Resolution FPS Bitrate + Codec Verbose Logging Warning: This logs a LOT! Don\'t enable for regular use. 360p @@ -80,6 +82,8 @@ 30 60 Auto (%d) + H264 + H265 (PS5 only) Swap Cross/Moon and Box/Pyramid Buttons Swap face buttons if default mapping is incorrect (e.g. for 8BitDo controllers) Are you sure you want to delete the registered console %s with ID %s? @@ -103,4 +107,5 @@ stream_resolution stream_fps stream_bitrate + stream_codec diff --git a/android/app/src/main/res/xml/preferences.xml b/android/app/src/main/res/xml/preferences.xml index d656347..0416f6e 100644 --- a/android/app/src/main/res/xml/preferences.xml +++ b/android/app/src/main/res/xml/preferences.xml @@ -52,6 +52,12 @@ app:key="@string/preferences_bitrate_key" app:title="@string/preferences_bitrate_title" app:icon="@drawable/ic_bitrate"/> + + Date: Mon, 28 Dec 2020 22:43:19 +0100 Subject: [PATCH 122/237] Make Android PIN Instruction depend on Console --- .../main/java/com/metallic/chiaki/regist/RegistActivity.kt | 2 ++ .../main/java/com/metallic/chiaki/regist/RegistViewModel.kt | 4 +++- .../chiaki/settings/SettingsRegisteredHostsAdapter.kt | 2 +- android/app/src/main/res/layout/activity_regist.xml | 4 ++-- android/app/src/main/res/values/strings.xml | 6 ++++-- 5 files changed, 12 insertions(+), 6 deletions(-) diff --git a/android/app/src/main/java/com/metallic/chiaki/regist/RegistActivity.kt b/android/app/src/main/java/com/metallic/chiaki/regist/RegistActivity.kt index 1332fdc..fb3ddaa 100644 --- a/android/app/src/main/java/com/metallic/chiaki/regist/RegistActivity.kt +++ b/android/app/src/main/java/com/metallic/chiaki/regist/RegistActivity.kt @@ -74,6 +74,8 @@ class RegistActivity: AppCompatActivity(), RevealActivity RegistViewModel.ConsoleVersion.PS4_LT_7 -> R.string.hint_regist_psn_online_id else -> R.string.hint_regist_psn_account_id }) + pinHelpBeforeTextView.setText(if(it.isPS5) R.string.regist_pin_instructions_ps5_before else R.string.regist_pin_instructions_ps4_before) + pinHelpNavigationTextView.setText(if(it.isPS5) R.string.regist_pin_instructions_ps5_navigation else R.string.regist_pin_instructions_ps4_navigation) }) } diff --git a/android/app/src/main/java/com/metallic/chiaki/regist/RegistViewModel.kt b/android/app/src/main/java/com/metallic/chiaki/regist/RegistViewModel.kt index 31c6bda..ab7253f 100644 --- a/android/app/src/main/java/com/metallic/chiaki/regist/RegistViewModel.kt +++ b/android/app/src/main/java/com/metallic/chiaki/regist/RegistViewModel.kt @@ -11,7 +11,9 @@ class RegistViewModel: ViewModel() PS5, PS4_GE_8, PS4_GE_7, - PS4_LT_7 + PS4_LT_7; + + val isPS5 get() = this == PS5 } val ps4Version = MutableLiveData(ConsoleVersion.PS5) diff --git a/android/app/src/main/java/com/metallic/chiaki/settings/SettingsRegisteredHostsAdapter.kt b/android/app/src/main/java/com/metallic/chiaki/settings/SettingsRegisteredHostsAdapter.kt index 84eebe5..57a06ce 100644 --- a/android/app/src/main/java/com/metallic/chiaki/settings/SettingsRegisteredHostsAdapter.kt +++ b/android/app/src/main/java/com/metallic/chiaki/settings/SettingsRegisteredHostsAdapter.kt @@ -29,7 +29,7 @@ class SettingsRegisteredHostsAdapter: RecyclerView.Adapter diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml index 721fe31..0f54522 100644 --- a/android/app/src/main/res/values/strings.xml +++ b/android/app/src/main/res/values/strings.xml @@ -21,8 +21,10 @@ App: %s\nTitle ID: %s Register Console Add Console Manually - On the PS4, navigate to - Settings →\nRemote Play Connection Settings →\nAdd Device + On the PS4, navigate to + Settings →\nRemote Play Connection Settings →\nAdd Device + On the PS5, navigate to + Settings →\nSystem → Remote Play →\nLink Device to obtain the PIN Register Console Host From 12c14fed05ed894f7394a299ae050ce364f31709 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Mon, 28 Dec 2020 22:53:36 +0100 Subject: [PATCH 123/237] Fix GUI Settings Migration --- gui/src/settings.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/gui/src/settings.cpp b/gui/src/settings.cpp index 715c2d5..3b91077 100644 --- a/gui/src/settings.cpp +++ b/gui/src/settings.cpp @@ -17,8 +17,10 @@ static void MigrateSettingsTo2(QSettings *settings) QMap host; for(QString k : settings->allKeys()) host[k] = settings->value(k); + hosts.append(host); } settings->endArray(); + settings->remove("registered_hosts"); settings->beginWriteArray("registered_hosts"); int i=0; for(const auto &host : hosts) @@ -53,12 +55,13 @@ static void MigrateSettings(QSettings *settings) CHIAKI_LOGE(NULL, "Settings version %d is higher than application one (%d)", version_prev, SETTINGS_VERSION); return; } - while(version_prev < 1) + while(version_prev < SETTINGS_VERSION) { version_prev++; switch(version_prev) { case 2: + CHIAKI_LOGI(NULL, "Migrating settings to 2"); MigrateSettingsTo2(settings); break; default: From f4d255eb5b02057c89b080a252fb060592299fe7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Mon, 28 Dec 2020 23:08:37 +0100 Subject: [PATCH 124/237] Fix sockaddr include in discoveryservice.c --- lib/src/discoveryservice.c | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/src/discoveryservice.c b/lib/src/discoveryservice.c index 463c65f..09f31ef 100644 --- a/lib/src/discoveryservice.c +++ b/lib/src/discoveryservice.c @@ -5,6 +5,12 @@ #include #include +#ifdef _WIN32 +#include +#else +#include +#endif + static void *discovery_service_thread_func(void *user); static void discovery_service_ping(ChiakiDiscoveryService *service); static void discovery_service_drop_old_hosts(ChiakiDiscoveryService *service); From d10d2ac56254fd1e9114ec59fbe32f6600e2a70d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Mon, 28 Dec 2020 23:11:46 +0100 Subject: [PATCH 125/237] Fix Compatibility with older Qt --- gui/res/console-ps4.svg | 9 ++++----- gui/src/servericonwidget.cpp | 2 +- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/gui/res/console-ps4.svg b/gui/res/console-ps4.svg index 1591574..22882ee 100644 --- a/gui/res/console-ps4.svg +++ b/gui/res/console-ps4.svg @@ -25,19 +25,18 @@ + id="layer1"> diff --git a/gui/src/servericonwidget.cpp b/gui/src/servericonwidget.cpp index 45d85a5..2b720d0 100644 --- a/gui/src/servericonwidget.cpp +++ b/gui/src/servericonwidget.cpp @@ -37,7 +37,7 @@ void ServerIconWidget::paintEvent(QPaintEvent *event) painter.setRenderHint(QPainter::Antialiasing); auto render_element = [&view_box, &icon_rect, this](QPainter &painter, const QString &id) { - QRectF src = svg_renderer.transformForElement(id).mapRect(svg_renderer.boundsOnElement(id)); + QRectF src = /*svg_renderer.transformForElement(id).mapRect(*/svg_renderer.boundsOnElement(id)/*)*/; QRectF dst = src.translated(-view_box.left(), -view_box.top()); dst = QRectF( icon_rect.width() * dst.left() / view_box.width(), From 308d6043f43730c8e6400d506ddf16ab87dc1ce0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Tue, 29 Dec 2020 10:23:27 +0100 Subject: [PATCH 126/237] Update PS4 References --- README.md | 14 +++++++------- cli/src/main.c | 2 +- gui/chiaki.desktop | 2 +- gui/re.chiaki.Chiaki.appdata.xml | 4 ++-- gui/src/streamwindow.cpp | 2 +- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 6648e75..b84b73d 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ [![AppVeyor Build status](https://ci.appveyor.com/api/projects/status/c81ogebvsmo43dd3?svg=true)](https://ci.appveyor.com/project/thestr4ng3r/chiaki) [![builds.sr.ht Status](https://builds.sr.ht/~thestr4ng3r/chiaki.svg)](https://builds.sr.ht/~thestr4ng3r/chiaki?) -Chiaki is a Free and Open Source Software Client for PlayStation 4 Remote Play +Chiaki is a Free and Open Source Software Client for PlayStation 4 and PlayStation 5 Remote Play for Linux, FreeBSD, OpenBSD, Android, macOS, Windows, Nintendo Switch and potentially even more platforms. ![Screenshot](assets/screenshot.png) @@ -51,11 +51,11 @@ For more detailed platform-specific instructions, see [doc/platform-build.md](do in ## Usage -If your PS4 is on your local network, is turned on or in standby mode and does not have Discovery explicitly disabled, Chiaki should find it. +If your Console is on your local network, is turned on or in standby mode and does not have Discovery explicitly disabled, Chiaki should find it. Otherwise, you can add it manually. -To do so, click the "+" icon in the top right, and enter your PS4's IP address. +To do so, click the "+" icon in the top right, and enter your Console's IP address. -You will then need to register your PS4 with Chiaki. You will need two more pieces of information to do this. +You will then need to register your Console with Chiaki. You will need two more pieces of information to do this. ### Obtaining your PSN AccountID @@ -66,10 +66,10 @@ Simply run it in a terminal and follow the instructions. Once you know your ID, ### Obtaining a Registration PIN -To register a PS4 with a PIN, it must be put into registration mode. To do this, on your PS4, simply go to: -Settings -> Remote Play (ensure this is ticked) -> Add Device +To register a Console with a PIN, it must be put into registration mode. To do this on a PS4, simply go to: +Settings -> Remote Play -> Add Device, or on a PS5: Settings -> System -> Remote Play -> Link Device. -You can now double-click your PS4 in Chiaki's main window to start Remote Play. +You can now double-click your Console in Chiaki's main window to start Remote Play. ## Joining the Community or Getting Help diff --git a/cli/src/main.c b/cli/src/main.c index 41c157e..3f7747a 100644 --- a/cli/src/main.c +++ b/cli/src/main.c @@ -9,7 +9,7 @@ #include static const char doc[] = - "CLI for Chiaki (PS4 Remote Play Client)" + "CLI for Chiaki (PlayStation Remote Play Client)" "\v" "Supported commands are:\n" " discover Discover Consoles.\n" diff --git a/gui/chiaki.desktop b/gui/chiaki.desktop index 80ee51b..53f3be2 100644 --- a/gui/chiaki.desktop +++ b/gui/chiaki.desktop @@ -1,7 +1,7 @@ [Desktop Entry] Type=Application Name=Chiaki -Comment=PlayStation 4 Remote Play Client +Comment=PlayStation Remote Play Client Exec=chiaki Icon=chiaki Categories=Game; diff --git a/gui/re.chiaki.Chiaki.appdata.xml b/gui/re.chiaki.Chiaki.appdata.xml index ba494cc..adb10ba 100644 --- a/gui/re.chiaki.Chiaki.appdata.xml +++ b/gui/re.chiaki.Chiaki.appdata.xml @@ -4,7 +4,7 @@ re.chiaki.Chiaki chiaki.desktop Chiaki - Free and Open Source Client for PlayStation 4 Remote Play + Free and Open Source Client for PlayStation Remote Play CC0-1.0 AGPL-3.0-only Florian Märkl @@ -12,7 +12,7 @@ https://git.sr.ht/~thestr4ng3r/chiaki/tree/master/item/README.md#usage

- Chiaki is a Free and Open Source Client for PlayStation 4 Remote Play. It can be used to play in real time on a PlayStation 4 as long as there is a network connection. + Chiaki is a Free and Open Source Client for PlayStation 4 and PlayStation 5 Remote Play. It can be used to play in real time on a PlayStation as long as there is a network connection.

diff --git a/gui/src/streamwindow.cpp b/gui/src/streamwindow.cpp index 58f7392..feff758 100644 --- a/gui/src/streamwindow.cpp +++ b/gui/src/streamwindow.cpp @@ -113,7 +113,7 @@ void StreamWindow::closeEvent(QCloseEvent *event) switch(connect_info.settings->GetDisconnectAction()) { case DisconnectAction::Ask: { - auto res = QMessageBox::question(this, tr("Disconnect Session"), tr("Do you want the PS4 to go into sleep mode?"), + auto res = QMessageBox::question(this, tr("Disconnect Session"), tr("Do you want the Console to go into sleep mode?"), QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel); switch(res) { From 88230d54d7879aa18c13a5436d6874d2acfd69ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Tue, 29 Dec 2020 11:12:32 +0100 Subject: [PATCH 127/237] Fix Manual Host ID reconstruction in GUI --- gui/src/settings.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gui/src/settings.cpp b/gui/src/settings.cpp index 3b91077..17f7d83 100644 --- a/gui/src/settings.cpp +++ b/gui/src/settings.cpp @@ -301,7 +301,7 @@ void Settings::LoadManualHosts() if(host.GetID() < 0) continue; if(manual_hosts_id_next <= host.GetID()) - manual_hosts_id_next = host.GetID(); + manual_hosts_id_next = host.GetID() + 1; manual_hosts[host.GetID()] = host; } settings.endArray(); From 526286c5d7fcd7497f9c80bb31a7dcb63980b10b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Tue, 29 Dec 2020 10:33:16 +0100 Subject: [PATCH 128/237] Update FFMPEG and enable H265 Decoder --- scripts/build-ffmpeg.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/build-ffmpeg.sh b/scripts/build-ffmpeg.sh index 00289fd..4761520 100755 --- a/scripts/build-ffmpeg.sh +++ b/scripts/build-ffmpeg.sh @@ -5,10 +5,10 @@ cd "./$1" shift ROOT="`pwd`" -TAG=n4.2 +TAG=n4.3.1 git clone https://git.ffmpeg.org/ffmpeg.git --depth 1 -b $TAG && cd ffmpeg || exit 1 -./configure --disable-all --enable-avcodec --enable-decoder=h264 --enable-hwaccel=h264_vaapi --prefix="$ROOT/ffmpeg-prefix" "$@" || exit 1 +./configure --disable-all --enable-avcodec --enable-decoder=h264 --enable-decoder=hevc --enable-hwaccel=h264_vaapi --prefix="$ROOT/ffmpeg-prefix" "$@" || exit 1 make -j4 || exit 1 make install || exit 1 From 5e3a19fccc4dc14bce619ae95e3e3338d26a8369 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Tue, 29 Dec 2020 11:33:15 +0100 Subject: [PATCH 129/237] Fix devicePixelRatio in ServerIconWidget --- gui/src/servericonwidget.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/gui/src/servericonwidget.cpp b/gui/src/servericonwidget.cpp index 2b720d0..6eb3d99 100644 --- a/gui/src/servericonwidget.cpp +++ b/gui/src/servericonwidget.cpp @@ -66,6 +66,7 @@ void ServerIconWidget::paintEvent(QPaintEvent *event) (int)(devicePixelRatioF() * height()), QImage::Format_ARGB32_Premultiplied); { + temp_image.setDevicePixelRatio(devicePixelRatioF()); temp_image.fill(QColor(0, 0, 0, 0)); QPainter temp_painter(&temp_image); render_element(temp_painter, "console"); From 3a7ac73c9a3137632828318c8b5698bf040db24e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Tue, 29 Dec 2020 12:13:26 +0100 Subject: [PATCH 130/237] Do not show Private Keys in Switch GUI --- switch/src/gui.cpp | 19 ++++--------------- 1 file changed, 4 insertions(+), 15 deletions(-) diff --git a/switch/src/gui.cpp b/switch/src/gui.cpp index 6d4ef2a..e49852b 100644 --- a/switch/src/gui.cpp +++ b/switch/src/gui.cpp @@ -447,24 +447,13 @@ bool MainApplication::BuildConfigurationMenu(brls::List *ls, Host *host) ls->addView(host_name); std::string host_addr_string = settings->GetHostAddr(host); - brls::ListItem *host_addr = new brls::ListItem("PS4 Address"); + brls::ListItem *host_addr = new brls::ListItem("PS Address"); host_addr->setValue(host_addr_string.c_str()); ls->addView(host_addr); - std::string host_rp_regist_key_string = settings->GetHostRPRegistKey(host); - brls::ListItem *host_rp_regist_key = new brls::ListItem("RP Register Key"); - host_rp_regist_key->setValue(host_rp_regist_key_string.c_str()); - ls->addView(host_rp_regist_key); - - std::string host_rp_key_string = settings->GetHostRPKey(host); - brls::ListItem *host_rp_key = new brls::ListItem("RP Key"); - host_rp_key->setValue(host_rp_key_string.c_str()); - ls->addView(host_rp_key); - - std::string host_rp_key_type_string = std::to_string(settings->GetHostRPKeyType(host)); - brls::ListItem *host_rp_key_type = new brls::ListItem("RP Key type"); - host_rp_key_type->setValue(host_rp_key_type_string.c_str()); - ls->addView(host_rp_key_type); + brls::ListItem *host_regist_state_item = new brls::ListItem("Register Status"); + host_regist_state_item->setValue(!settings->GetHostRPKey(host).empty() ? "registered" : "unregistered"); + ls->addView(host_regist_state_item); } return true; From 854c600954a0a0b9d83ed2e58b53a20449e2e743 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Tue, 29 Dec 2020 11:54:47 +0100 Subject: [PATCH 131/237] Version 2.0.0 --- CMakeLists.txt | 4 ++-- android/app/build.gradle | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 78662a4..3bddb7f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -30,8 +30,8 @@ option(CHIAKI_CLI_ARGP_STANDALONE "Search for standalone argp lib for CLI" OFF) tri_option(CHIAKI_USE_SYSTEM_JERASURE "Use system-provided jerasure instead of submodule" AUTO) tri_option(CHIAKI_USE_SYSTEM_NANOPB "Use system-provided nanopb instead of submodule" AUTO) -set(CHIAKI_VERSION_MAJOR 1) -set(CHIAKI_VERSION_MINOR 3) +set(CHIAKI_VERSION_MAJOR 2) +set(CHIAKI_VERSION_MINOR 0) set(CHIAKI_VERSION_PATCH 0) set(CHIAKI_VERSION ${CHIAKI_VERSION_MAJOR}.${CHIAKI_VERSION_MINOR}.${CHIAKI_VERSION_PATCH}) diff --git a/android/app/build.gradle b/android/app/build.gradle index 8e32ae3..ee70e84 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -24,7 +24,7 @@ android { applicationId "com.metallic.chiaki" minSdkVersion 21 targetSdkVersion 30 - versionCode 7 + versionCode 8 versionName chiakiVersion externalNativeBuild { cmake { From 1ce2dbd5d2f8135561c0e03bcccb296c08f4ac10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Tue, 29 Dec 2020 20:11:41 +0100 Subject: [PATCH 132/237] Fix Android Database Migration --- .../src/main/java/com/metallic/chiaki/common/AppDatabase.kt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/android/app/src/main/java/com/metallic/chiaki/common/AppDatabase.kt b/android/app/src/main/java/com/metallic/chiaki/common/AppDatabase.kt index 4d4738c..8398f62 100644 --- a/android/app/src/main/java/com/metallic/chiaki/common/AppDatabase.kt +++ b/android/app/src/main/java/com/metallic/chiaki/common/AppDatabase.kt @@ -23,9 +23,11 @@ val MIGRATION_1_2 = object : Migration(1, 2) { override fun migrate(database: SupportSQLiteDatabase) { - database.execSQL("ALTER TABLE registered_host RENAME ps4_mac TO server_mac") - database.execSQL("ALTER TABLE registered_host RENAME ps4_nickname TO server_nickname") database.execSQL("ALTER TABLE registered_host ADD target INTEGER NOT NULL DEFAULT 1000") + database.execSQL("CREATE TABLE `new_registered_host` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `target` INTEGER NOT NULL, `ap_ssid` TEXT, `ap_bssid` TEXT, `ap_key` TEXT, `ap_name` TEXT, `server_mac` INTEGER NOT NULL, `server_nickname` TEXT, `rp_regist_key` BLOB NOT NULL, `rp_key_type` INTEGER NOT NULL, `rp_key` BLOB NOT NULL)"); + database.execSQL("INSERT INTO `new_registered_host` SELECT `id`, `target`, `ap_ssid`, `ap_bssid`, `ap_key`, `ap_name`, `ps4_mac`, `ps4_nickname`, `rp_regist_key`, `rp_key_type`, `rp_key` FROM `registered_host`") + database.execSQL("DROP TABLE registered_host") + database.execSQL("ALTER TABLE new_registered_host RENAME TO registered_host") } } From 9e698dd7c4e4011ff6e136741abef5cf4b32527c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Tue, 29 Dec 2020 20:12:12 +0100 Subject: [PATCH 133/237] Version 2.0.1 --- CMakeLists.txt | 2 +- android/app/build.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 3bddb7f..a6beae8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -32,7 +32,7 @@ tri_option(CHIAKI_USE_SYSTEM_NANOPB "Use system-provided nanopb instead of submo set(CHIAKI_VERSION_MAJOR 2) set(CHIAKI_VERSION_MINOR 0) -set(CHIAKI_VERSION_PATCH 0) +set(CHIAKI_VERSION_PATCH 1) set(CHIAKI_VERSION ${CHIAKI_VERSION_MAJOR}.${CHIAKI_VERSION_MINOR}.${CHIAKI_VERSION_PATCH}) set(CPACK_PACKAGE_NAME "chiaki") diff --git a/android/app/build.gradle b/android/app/build.gradle index ee70e84..fde2388 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -24,7 +24,7 @@ android { applicationId "com.metallic.chiaki" minSdkVersion 21 targetSdkVersion 30 - versionCode 8 + versionCode 9 versionName chiakiVersion externalNativeBuild { cmake { From 89c3175d717917a0f77c243872df000d93f11142 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Wed, 30 Dec 2020 12:37:28 +0100 Subject: [PATCH 134/237] Add fallback if getnameinfo fails --- lib/include/chiaki/session.h | 2 +- lib/src/session.c | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/include/chiaki/session.h b/lib/include/chiaki/session.h index db7bf02..1706e27 100644 --- a/lib/include/chiaki/session.h +++ b/lib/include/chiaki/session.h @@ -155,7 +155,7 @@ typedef struct chiaki_session_t bool ps5; struct addrinfo *host_addrinfos; struct addrinfo *host_addrinfo_selected; - char hostname[128]; + char hostname[256]; char regist_key[CHIAKI_RPCRYPT_KEY_SIZE]; uint8_t morning[CHIAKI_RPCRYPT_KEY_SIZE]; uint8_t did[CHIAKI_RP_DID_SIZE]; diff --git a/lib/src/session.c b/lib/src/session.c index de2f4e2..a57ea1d 100644 --- a/lib/src/session.c +++ b/lib/src/session.c @@ -596,11 +596,11 @@ static ChiakiErrorCode session_thread_request_session(ChiakiSession *session, Ch set_port(sa, htons(SESSION_PORT)); // TODO: this can block, make cancelable somehow - int r = getnameinfo(sa, (socklen_t)ai->ai_addrlen, session->connect_info.hostname, sizeof(session->connect_info.hostname), NULL, 0, 0); + int r = getnameinfo(sa, (socklen_t)ai->ai_addrlen, session->connect_info.hostname, sizeof(session->connect_info.hostname), NULL, 0, NI_NUMERICHOST); if(r != 0) { - free(sa); - continue; + CHIAKI_LOGE(session->log, "getnameinfo failed with %s, filling the hostname with fallback", gai_strerror(r)); + memcpy(session->connect_info.hostname, "unknown", 8); } CHIAKI_LOGI(session->log, "Trying to request session from %s:%d", session->connect_info.hostname, SESSION_PORT); From 85d9594ebce06c7c888486760fdb3cec95277dca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Wed, 30 Dec 2020 16:11:06 +0100 Subject: [PATCH 135/237] Add DualSense to Setsu --- setsu/src/setsu.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/setsu/src/setsu.c b/setsu/src/setsu.c index f899d07..819b174 100644 --- a/setsu/src/setsu.c +++ b/setsu/src/setsu.c @@ -157,8 +157,9 @@ static bool is_device_interesting(struct udev_device *dev) { static const uint32_t device_ids[] = { // vendor id, model id - 0x054c, 0x05c4, // DualShock 4 Gen 1 USB - 0x054c, 0x09cc // DualShock 4 Gen 2 USB + 0x054c, 0x05c4, // DualShock 4 Gen 1 + 0x054c, 0x09cc, // DualShock 4 Gen 2 + 0x54c, 0x0ce6 // DualSense }; // Filter mouse-device (/dev/input/mouse*) away and only keep the evdev (/dev/input/event*) one: From fbb19f94ead9472f21890a730484f0ea73589a08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Thu, 31 Dec 2020 21:24:30 +0100 Subject: [PATCH 136/237] Fix err in streamsession.cpp for pi --- gui/src/streamsession.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/gui/src/streamsession.cpp b/gui/src/streamsession.cpp index fedae10..f5c4c89 100644 --- a/gui/src/streamsession.cpp +++ b/gui/src/streamsession.cpp @@ -52,6 +52,7 @@ StreamSession::StreamSession(const StreamSessionConnectInfo &connect_info, QObje audio_io(nullptr) { connected = false; + ChiakiErrorCode err; #if CHIAKI_LIB_ENABLE_PI_DECODER if(connect_info.decoder == Decoder::Pi) @@ -66,7 +67,7 @@ StreamSession::StreamSession(const StreamSessionConnectInfo &connect_info, QObje ffmpeg_decoder = new ChiakiFfmpegDecoder; ChiakiLogSniffer sniffer; chiaki_log_sniffer_init(&sniffer, CHIAKI_LOG_ALL, GetChiakiLog()); - ChiakiErrorCode err = chiaki_ffmpeg_decoder_init(ffmpeg_decoder, + err = chiaki_ffmpeg_decoder_init(ffmpeg_decoder, chiaki_log_sniffer_get_log(&sniffer), chiaki_target_is_ps5(connect_info.target) ? connect_info.video_profile.codec : CHIAKI_CODEC_H264, connect_info.hw_decoder.isEmpty() ? NULL : connect_info.hw_decoder.toUtf8().constData(), From 81984b7d48b7459155353833b04016aa0f83a4e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Thu, 31 Dec 2020 23:45:47 +0100 Subject: [PATCH 137/237] Add Rumble to Lib --- gui/src/streamsession.cpp | 7 +++++++ lib/include/chiaki/session.h | 8 ++++++++ lib/include/chiaki/takion.h | 1 + lib/src/streamconnection.c | 35 +++++++++++++++++++++++++++++++++-- lib/src/takion.c | 5 +++-- 5 files changed, 52 insertions(+), 4 deletions(-) diff --git a/gui/src/streamsession.cpp b/gui/src/streamsession.cpp index f5c4c89..886806f 100644 --- a/gui/src/streamsession.cpp +++ b/gui/src/streamsession.cpp @@ -381,6 +381,13 @@ void StreamSession::Event(ChiakiEvent *event) case CHIAKI_EVENT_LOGIN_PIN_REQUEST: emit LoginPINRequested(event->login_pin_request.pin_incorrect); break; + case CHIAKI_EVENT_RUMBLE: + // TODO + //CHIAKI_LOGD(GetChiakiLog(), "Rumble %#02x, %#02x, %#02x", + // event->rumble.unknown, event->rumble.left, event->rumble.right); + break; + default: + break; } } diff --git a/lib/include/chiaki/session.h b/lib/include/chiaki/session.h index 1706e27..a3480c1 100644 --- a/lib/include/chiaki/session.h +++ b/lib/include/chiaki/session.h @@ -114,6 +114,12 @@ typedef struct chiaki_audio_stream_info_event_t ChiakiAudioHeader audio_header; } ChiakiAudioStreamInfoEvent; +typedef struct chiaki_rumble_event_t +{ + uint8_t unknown; + uint8_t left; + uint8_t right; +} ChiakiRumbleEvent; typedef enum { CHIAKI_EVENT_CONNECTED, @@ -121,6 +127,7 @@ typedef enum { CHIAKI_EVENT_KEYBOARD_OPEN, CHIAKI_EVENT_KEYBOARD_TEXT_CHANGE, CHIAKI_EVENT_KEYBOARD_REMOTE_CLOSE, + CHIAKI_EVENT_RUMBLE, CHIAKI_EVENT_QUIT, } ChiakiEventType; @@ -131,6 +138,7 @@ typedef struct chiaki_event_t { ChiakiQuitEvent quit; ChiakiKeyboardEvent keyboard; + ChiakiRumbleEvent rumble; struct { bool pin_incorrect; // false on first request, true if the pin entered before was incorrect diff --git a/lib/include/chiaki/takion.h b/lib/include/chiaki/takion.h index 3f7e96f..1715842 100644 --- a/lib/include/chiaki/takion.h +++ b/lib/include/chiaki/takion.h @@ -26,6 +26,7 @@ extern "C" { typedef enum chiaki_takion_message_data_type_t { CHIAKI_TAKION_MESSAGE_DATA_TYPE_PROTOBUF = 0, + CHIAKI_TAKION_MESSAGE_DATA_TYPE_RUMBLE = 7, CHIAKI_TAKION_MESSAGE_DATA_TYPE_9 = 9 } ChiakiTakionMessageDataType; diff --git a/lib/src/streamconnection.c b/lib/src/streamconnection.c index c8ed43c..28b22dc 100644 --- a/lib/src/streamconnection.c +++ b/lib/src/streamconnection.c @@ -45,6 +45,8 @@ void chiaki_session_send_event(ChiakiSession *session, ChiakiEvent *event); static void stream_connection_takion_cb(ChiakiTakionEvent *event, void *user); static void stream_connection_takion_data(ChiakiStreamConnection *stream_connection, ChiakiTakionMessageDataType data_type, uint8_t *buf, size_t buf_size); +static void stream_connection_takion_data_protobuf(ChiakiStreamConnection *stream_connection, uint8_t *buf, size_t buf_size); +static void stream_connection_takion_data_rumble(ChiakiStreamConnection *stream_connection, uint8_t *buf, size_t buf_size); static ChiakiErrorCode stream_connection_send_big(ChiakiStreamConnection *stream_connection); static ChiakiErrorCode stream_connection_send_disconnect(ChiakiStreamConnection *stream_connection); static void stream_connection_takion_data_idle(ChiakiStreamConnection *stream_connection, uint8_t *buf, size_t buf_size); @@ -368,9 +370,21 @@ static void stream_connection_takion_cb(ChiakiTakionEvent *event, void *user) static void stream_connection_takion_data(ChiakiStreamConnection *stream_connection, ChiakiTakionMessageDataType data_type, uint8_t *buf, size_t buf_size) { - if(data_type != CHIAKI_TAKION_MESSAGE_DATA_TYPE_PROTOBUF) - return; + switch(data_type) + { + case CHIAKI_TAKION_MESSAGE_DATA_TYPE_PROTOBUF: + stream_connection_takion_data_protobuf(stream_connection, buf, buf_size); + break; + case CHIAKI_TAKION_MESSAGE_DATA_TYPE_RUMBLE: + stream_connection_takion_data_rumble(stream_connection, buf, buf_size); + break; + default: + break; + } +} +static void stream_connection_takion_data_protobuf(ChiakiStreamConnection *stream_connection, uint8_t *buf, size_t buf_size) +{ chiaki_mutex_lock(&stream_connection->state_mutex); switch(stream_connection->state) { @@ -385,6 +399,23 @@ static void stream_connection_takion_data(ChiakiStreamConnection *stream_connect break; } chiaki_mutex_unlock(&stream_connection->state_mutex); + +} + +static void stream_connection_takion_data_rumble(ChiakiStreamConnection *stream_connection, uint8_t *buf, size_t buf_size) +{ + if(buf_size < 3) + { + CHIAKI_LOGE(stream_connection->log, "StreamConnection got rumble packet with size %#llx < 3", + (unsigned long long)buf_size); + return; + } + ChiakiEvent event = { 0 }; + event.type = CHIAKI_EVENT_RUMBLE; + event.rumble.unknown = buf[0]; + event.rumble.left = buf[1]; + event.rumble.right = buf[2]; + chiaki_session_send_event(stream_connection->session, &event); } static void stream_connection_takion_data_handle_disconnect(ChiakiStreamConnection *stream_connection, uint8_t *buf, size_t buf_size) diff --git a/lib/src/takion.c b/lib/src/takion.c index 48a1537..fe3e95b 100644 --- a/lib/src/takion.c +++ b/lib/src/takion.c @@ -56,7 +56,6 @@ typedef enum takion_packet_type_t { TAKION_PACKET_TYPE_HANDSHAKE = 4, TAKION_PACKET_TYPE_CONGESTION = 5, TAKION_PACKET_TYPE_FEEDBACK_STATE = 6, - TAKION_PACKET_TYPE_RUMBLE_EVENT = 7, TAKION_PACKET_TYPE_CLIENT_INFO = 8, TAKION_PACKET_TYPE_PAD_INFO_EVENT = 9 } TakionPacketType; @@ -961,7 +960,9 @@ static void takion_flush_data_queue(ChiakiTakion *takion) if(zero_a != 0) CHIAKI_LOGW(takion->log, "Takion received data with unexpected nonzero %#x at buf+6", zero_a); - if(data_type != CHIAKI_TAKION_MESSAGE_DATA_TYPE_PROTOBUF && data_type != CHIAKI_TAKION_MESSAGE_DATA_TYPE_9) + if(data_type != CHIAKI_TAKION_MESSAGE_DATA_TYPE_PROTOBUF + && data_type != CHIAKI_TAKION_MESSAGE_DATA_TYPE_RUMBLE + && data_type != CHIAKI_TAKION_MESSAGE_DATA_TYPE_9) { CHIAKI_LOGW(takion->log, "Takion received data with unexpected data type %#x", data_type); chiaki_log_hexdump(takion->log, CHIAKI_LOG_WARNING, entry->packet_buf, entry->packet_size); From 6c46920adbe2b8906cb2e4285b38d5c997332ef8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Fri, 1 Jan 2021 11:09:13 +0100 Subject: [PATCH 138/237] Fix some Warnings --- test/reorderqueue.c | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/test/reorderqueue.c b/test/reorderqueue.c index e5a90c9..f0fd5fe 100644 --- a/test/reorderqueue.c +++ b/test/reorderqueue.c @@ -16,7 +16,7 @@ typedef struct drop_record_t static void drop(uint64_t seq_num, void *elem_user, void *cb_user) { DropRecord *record = cb_user; - uint64_t v = (uint64_t)elem_user; + uint64_t v = (uint64_t)(size_t)elem_user; if(v > DROP_RECORD_MAX) { record->failed = true; @@ -58,7 +58,7 @@ static MunitResult test_reorder_queue_16(const MunitParameter params[], void *te munit_assert_uint64(chiaki_reorder_queue_count(&queue), ==, 0); munit_assert(!drop_record.failed); munit_assert_uint64(drop_record.count[0], ==, 0); - munit_assert_uint64((uint64_t)user, ==, 0); + munit_assert_uint64((uint64_t)(size_t)user, ==, 0); munit_assert_uint64(seq_num, ==, 42); // push outdated @@ -105,22 +105,22 @@ static MunitResult test_reorder_queue_16(const MunitParameter params[], void *te pulled = chiaki_reorder_queue_pull(&queue, &seq_num, &user); munit_assert(pulled); munit_assert_uint64(seq_num, ==, 44); - munit_assert_uint64((uint64_t)user, ==, 3); + munit_assert_uint64((uint64_t)(size_t)user, ==, 3); pulled = chiaki_reorder_queue_pull(&queue, &seq_num, &user); munit_assert(pulled); munit_assert_uint64(seq_num, ==, 45); - munit_assert_uint64((uint64_t)user, ==, 2); + munit_assert_uint64((uint64_t)(size_t)user, ==, 2); pulled = chiaki_reorder_queue_pull(&queue, &seq_num, &user); munit_assert(pulled); munit_assert_uint64(seq_num, ==, 46); - munit_assert_uint64((uint64_t)user, ==, 1); + munit_assert_uint64((uint64_t)(size_t)user, ==, 1); pulled = chiaki_reorder_queue_pull(&queue, &seq_num, &user); munit_assert(pulled); munit_assert_uint64(seq_num, ==, 47); - munit_assert_uint64((uint64_t)user, ==, 5); + munit_assert_uint64((uint64_t)(size_t)user, ==, 5); // should be empty now again pulled = chiaki_reorder_queue_pull(&queue, &seq_num, &user); @@ -142,7 +142,7 @@ static MunitResult test_reorder_queue_16(const MunitParameter params[], void *te pulled = chiaki_reorder_queue_pull(&queue, &seq_num, &user); munit_assert(pulled); munit_assert_uint64(seq_num, ==, 1337); - munit_assert_uint64((uint64_t)user, ==, 6); + munit_assert_uint64((uint64_t)(size_t)user, ==, 6); munit_assert_uint64(chiaki_reorder_queue_count(&queue), ==, 0); // same as before, but with an element in the queue that will be dropped @@ -163,7 +163,7 @@ static MunitResult test_reorder_queue_16(const MunitParameter params[], void *te pulled = chiaki_reorder_queue_pull(&queue, &seq_num, &user); munit_assert(pulled); munit_assert_uint64(seq_num, ==, 2000); - munit_assert_uint64((uint64_t)user, ==, 8); + munit_assert_uint64((uint64_t)(size_t)user, ==, 8); munit_assert_uint64(chiaki_reorder_queue_count(&queue), ==, 0); chiaki_reorder_queue_fini(&queue); @@ -182,4 +182,4 @@ MunitTest tests_reorder_queue[] = { NULL }, { NULL, NULL, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL } -}; \ No newline at end of file +}; From 3c2e9a0418f5fec6ea4bd656791db6e2a16522df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Fri, 1 Jan 2021 11:52:49 +0100 Subject: [PATCH 139/237] Use correct Target in GUI CLI Start --- gui/src/main.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/gui/src/main.cpp b/gui/src/main.cpp index 56a85f2..b6731c8 100644 --- a/gui/src/main.cpp +++ b/gui/src/main.cpp @@ -123,6 +123,7 @@ int real_main(int argc, char *argv[]) QString host = args[args.size()-1]; QByteArray morning; QByteArray regist_key; + ChiakiTarget target = CHIAKI_TARGET_PS4_10; if(parser.value(regist_key_option).isEmpty() && parser.value(morning_option).isEmpty()) { @@ -134,6 +135,7 @@ int real_main(int argc, char *argv[]) { morning = temphost.GetRPKey(); regist_key = temphost.GetRPRegistKey(); + target = temphost.GetTarget(); break; } printf("No configuration found for '%s'\n", args[1].toLocal8Bit().constData()); @@ -142,6 +144,7 @@ int real_main(int argc, char *argv[]) } else { + // TODO: explicit option for target regist_key = parser.value(regist_key_option).toUtf8(); if(regist_key.length() > sizeof(ChiakiConnectInfo::regist_key)) { @@ -161,8 +164,7 @@ int real_main(int argc, char *argv[]) return 1; } } - // TODO: target here - StreamSessionConnectInfo connect_info(&settings, CHIAKI_TARGET_PS4_10, host, regist_key, morning, parser.isSet(fullscreen_option)); + StreamSessionConnectInfo connect_info(&settings, target, host, regist_key, morning, parser.isSet(fullscreen_option)); return RunStream(app, connect_info); } #ifdef CHIAKI_ENABLE_CLI From 042e02eb3eac4ebc6594fd586b0850734354f980 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Fri, 1 Jan 2021 13:56:30 +0100 Subject: [PATCH 140/237] Add Rumble to GUI --- gui/include/controllermanager.h | 1 + gui/include/streamsession.h | 2 +- gui/src/controllermanager.cpp | 9 +++++++++ gui/src/streamsession.cpp | 14 ++++++++------ lib/include/chiaki/session.h | 4 ++-- 5 files changed, 21 insertions(+), 9 deletions(-) diff --git a/gui/include/controllermanager.h b/gui/include/controllermanager.h index 6ec6efb..18d9d72 100644 --- a/gui/include/controllermanager.h +++ b/gui/include/controllermanager.h @@ -73,6 +73,7 @@ class Controller : public QObject int GetDeviceID(); QString GetName(); ChiakiControllerState GetState(); + void SetRumble(uint8_t left, uint8_t right); signals: void StateChanged(); diff --git a/gui/include/streamsession.h b/gui/include/streamsession.h index c98758e..47dc217 100644 --- a/gui/include/streamsession.h +++ b/gui/include/streamsession.h @@ -92,13 +92,13 @@ class StreamSession : public QObject QMap key_map; void PushAudioFrame(int16_t *buf, size_t samples_count); - void Event(ChiakiEvent *event); #if CHIAKI_GUI_ENABLE_SETSU void HandleSetsuEvent(SetsuEvent *event); #endif private slots: void InitAudio(unsigned int channels, unsigned int rate); + void Event(ChiakiEvent *event); public: explicit StreamSession(const StreamSessionConnectInfo &connect_info, QObject *parent = nullptr); diff --git a/gui/src/controllermanager.cpp b/gui/src/controllermanager.cpp index 2f9a2aa..16804af 100644 --- a/gui/src/controllermanager.cpp +++ b/gui/src/controllermanager.cpp @@ -291,3 +291,12 @@ ChiakiControllerState Controller::GetState() #endif return state; } + +void Controller::SetRumble(uint8_t left, uint8_t right) +{ +#ifdef CHIAKI_GUI_ENABLE_SDL_GAMECONTROLLER + if(!controller) + return; + SDL_GameControllerRumble(controller, (uint16_t)left << 8, (uint16_t)right << 8, 5000); +#endif +} diff --git a/gui/src/streamsession.cpp b/gui/src/streamsession.cpp index 886806f..b1cbb47 100644 --- a/gui/src/streamsession.cpp +++ b/gui/src/streamsession.cpp @@ -313,13 +313,11 @@ void StreamSession::SendFeedbackState() ChiakiControllerState state; chiaki_controller_state_set_idle(&state); -#if CHIAKI_GUI_ENABLE_SDL_GAMECONTROLLER for(auto controller : controllers) { auto controller_state = controller->GetState(); chiaki_controller_state_or(&state, &state, &controller_state); } -#endif #if CHIAKI_GUI_ENABLE_SETSU chiaki_controller_state_or(&state, &state, &setsu_state); @@ -381,11 +379,15 @@ void StreamSession::Event(ChiakiEvent *event) case CHIAKI_EVENT_LOGIN_PIN_REQUEST: emit LoginPINRequested(event->login_pin_request.pin_incorrect); break; - case CHIAKI_EVENT_RUMBLE: - // TODO - //CHIAKI_LOGD(GetChiakiLog(), "Rumble %#02x, %#02x, %#02x", - // event->rumble.unknown, event->rumble.left, event->rumble.right); + case CHIAKI_EVENT_RUMBLE: { + uint8_t left = event->rumble.left; + uint8_t right = event->rumble.right; + QMetaObject::invokeMethod(this, [this, left, right]() { + for(auto controller : controllers) + controller->SetRumble(left, right); + }); break; + } default: break; } diff --git a/lib/include/chiaki/session.h b/lib/include/chiaki/session.h index a3480c1..3a60417 100644 --- a/lib/include/chiaki/session.h +++ b/lib/include/chiaki/session.h @@ -117,8 +117,8 @@ typedef struct chiaki_audio_stream_info_event_t typedef struct chiaki_rumble_event_t { uint8_t unknown; - uint8_t left; - uint8_t right; + uint8_t left; // low-frequency + uint8_t right; // high-frequency } ChiakiRumbleEvent; typedef enum { From 49d65ad14a418c0409fbb4f87484ddc3b8e17b05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Fri, 1 Jan 2021 15:24:53 +0100 Subject: [PATCH 141/237] Minor Style Fixes --- gui/src/avopenglwidget.cpp | 3 +- gui/src/controllermanager.cpp | 1 - gui/src/main.cpp | 2 +- gui/src/mainwindow.cpp | 4 +- gui/src/settings.cpp | 12 +- gui/src/settingsdialog.cpp | 12 +- gui/src/settingskeycapturedialog.cpp | 4 +- lib/src/audioreceiver.c | 8 +- lib/src/base64.c | 57 ++++--- lib/src/common.c | 6 +- lib/src/congestioncontrol.c | 2 +- lib/src/controller.c | 14 +- lib/src/ctrl.c | 7 +- lib/src/ecdh.c | 16 +- lib/src/frameprocessor.c | 4 +- lib/src/gkcrypt.c | 43 ++--- lib/src/http.c | 1 - lib/src/packetstats.c | 1 - lib/src/random.c | 6 +- lib/src/regist.c | 1 - lib/src/rpcrypt.c | 4 +- lib/src/senkusha.c | 2 - lib/src/session.c | 7 +- lib/src/stoppipe.c | 3 +- lib/src/streamconnection.c | 10 +- lib/src/takion.c | 26 +-- lib/src/thread.c | 20 +-- switch/src/discoverymanager.cpp | 18 +-- switch/src/gui.cpp | 16 +- switch/src/io.cpp | 4 +- switch/src/main.cpp | 6 +- switch/src/settings.cpp | 232 +++++++++++++-------------- 32 files changed, 248 insertions(+), 304 deletions(-) diff --git a/gui/src/avopenglwidget.cpp b/gui/src/avopenglwidget.cpp index 3d1d8eb..857af09 100644 --- a/gui/src/avopenglwidget.cpp +++ b/gui/src/avopenglwidget.cpp @@ -109,7 +109,6 @@ static const float vert_pos[] = { 1.0f, 1.0f }; - QSurfaceFormat AVOpenGLWidget::CreateSurfaceFormat() { QSurfaceFormat format; @@ -129,7 +128,7 @@ AVOpenGLWidget::AVOpenGLWidget(StreamSession *session, QWidget *parent) { enum AVPixelFormat pixel_format = chiaki_ffmpeg_decoder_get_pixel_format(session->GetFfmpegDecoder()); conversion_config = nullptr; - for(auto &cc: conversion_configs) + for(auto &cc : conversion_configs) { if(pixel_format == cc.pixel_format) { diff --git a/gui/src/controllermanager.cpp b/gui/src/controllermanager.cpp index 16804af..a78fd44 100644 --- a/gui/src/controllermanager.cpp +++ b/gui/src/controllermanager.cpp @@ -68,7 +68,6 @@ static QSet chiaki_motion_controller_guids({ "030000008f0e00001431000000000000", }); - static ControllerManager *instance = nullptr; #define UPDATE_INTERVAL_MS 4 diff --git a/gui/src/main.cpp b/gui/src/main.cpp index b6731c8..010e744 100644 --- a/gui/src/main.cpp +++ b/gui/src/main.cpp @@ -111,7 +111,7 @@ int real_main(int argc, char *argv[]) if(args[0] == "list") { for(const auto &host : settings.GetRegisteredHosts()) - printf("Host: %s \n", host.GetServerNickname().toLocal8Bit().constData()); + printf("Host: %s \n", host.GetServerNickname().toLocal8Bit().constData()); return 0; } if(args[0] == "stream") diff --git a/gui/src/mainwindow.cpp b/gui/src/mainwindow.cpp index 296626d..aa76ab3 100644 --- a/gui/src/mainwindow.cpp +++ b/gui/src/mainwindow.cpp @@ -167,7 +167,7 @@ MainWindow::MainWindow(Settings *settings, QWidget *parent) grid_widget->setContentsMargins(0, 0, 0, 0); resize(800, 600); - + connect(&discovery_manager, &DiscoveryManager::HostsUpdated, this, &MainWindow::UpdateDisplayServers); connect(settings, &Settings::RegisteredHostsUpdated, this, &MainWindow::UpdateDisplayServers); connect(settings, &Settings::ManualHostsUpdated, this, &MainWindow::UpdateDisplayServers); @@ -330,7 +330,7 @@ void MainWindow::UpdateDisplayServers() display_servers.append(server); } - + UpdateServerWidgets(); } diff --git a/gui/src/settings.cpp b/gui/src/settings.cpp index 17f7d83..6d296d1 100644 --- a/gui/src/settings.cpp +++ b/gui/src/settings.cpp @@ -88,10 +88,10 @@ uint32_t Settings::GetLogLevelMask() } static const QMap resolutions = { - { CHIAKI_VIDEO_RESOLUTION_PRESET_360p, "360p"}, - { CHIAKI_VIDEO_RESOLUTION_PRESET_540p, "540p"}, - { CHIAKI_VIDEO_RESOLUTION_PRESET_720p, "720p"}, - { CHIAKI_VIDEO_RESOLUTION_PRESET_1080p, "1080p"} + { CHIAKI_VIDEO_RESOLUTION_PRESET_360p, "360p" }, + { CHIAKI_VIDEO_RESOLUTION_PRESET_540p, "540p" }, + { CHIAKI_VIDEO_RESOLUTION_PRESET_720p, "720p" }, + { CHIAKI_VIDEO_RESOLUTION_PRESET_1080p, "1080p" } }; static const ChiakiVideoResolutionPreset resolution_default = CHIAKI_VIDEO_RESOLUTION_PRESET_720p; @@ -136,8 +136,8 @@ void Settings::SetBitrate(unsigned int bitrate) } static const QMap codecs = { - { CHIAKI_CODEC_H264, "h264"}, - { CHIAKI_CODEC_H265, "h265"} + { CHIAKI_CODEC_H264, "h264" }, + { CHIAKI_CODEC_H265, "h265" } }; static const ChiakiCodec codec_default = CHIAKI_CODEC_H265; diff --git a/gui/src/settingsdialog.cpp b/gui/src/settingsdialog.cpp index b8bd412..3f8856f 100644 --- a/gui/src/settingsdialog.cpp +++ b/gui/src/settingsdialog.cpp @@ -89,7 +89,7 @@ SettingsDialog::SettingsDialog(Settings *settings, QWidget *parent) : QDialog(pa connect(disconnect_action_combo_box, SIGNAL(currentIndexChanged(int)), this, SLOT(DisconnectActionSelected())); general_layout->addRow(tr("Action on Disconnect:"), disconnect_action_combo_box); - + audio_device_combo_box = new QComboBox(this); audio_device_combo_box->addItem(tr("Auto")); auto current_audio_device = settings->GetAudioOutDevice(); @@ -112,7 +112,7 @@ SettingsDialog::SettingsDialog(Settings *settings, QWidget *parent) : QDialog(pa auto available_devices = audio_devices_future_watcher->result(); while(audio_device_combo_box->count() > 1) // remove all but "Auto" audio_device_combo_box->removeItem(1); - for (QAudioDeviceInfo di : available_devices) + for(QAudioDeviceInfo di : available_devices) audio_device_combo_box->addItem(di.deviceName(), di.deviceName()); int audio_out_device_index = audio_device_combo_box->findData(settings->GetAudioOutDevice()); audio_device_combo_box->setCurrentIndex(audio_out_device_index < 0 ? 0 : audio_out_device_index); @@ -136,10 +136,10 @@ SettingsDialog::SettingsDialog(Settings *settings, QWidget *parent) : QDialog(pa resolution_combo_box = new QComboBox(this); static const QList> resolution_strings = { - { CHIAKI_VIDEO_RESOLUTION_PRESET_360p, "360p"}, - { CHIAKI_VIDEO_RESOLUTION_PRESET_540p, "540p"}, - { CHIAKI_VIDEO_RESOLUTION_PRESET_720p, "720p"}, - { CHIAKI_VIDEO_RESOLUTION_PRESET_1080p, "1080p (PS5 and PS4 Pro only)"} + { CHIAKI_VIDEO_RESOLUTION_PRESET_360p, "360p" }, + { CHIAKI_VIDEO_RESOLUTION_PRESET_540p, "540p" }, + { CHIAKI_VIDEO_RESOLUTION_PRESET_720p, "720p" }, + { CHIAKI_VIDEO_RESOLUTION_PRESET_1080p, "1080p (PS5 and PS4 Pro only)" } }; auto current_res = settings->GetResolution(); for(const auto &p : resolution_strings) diff --git a/gui/src/settingskeycapturedialog.cpp b/gui/src/settingskeycapturedialog.cpp index 3339813..a631444 100644 --- a/gui/src/settingskeycapturedialog.cpp +++ b/gui/src/settingskeycapturedialog.cpp @@ -7,7 +7,7 @@ #include #include -SettingsKeyCaptureDialog::SettingsKeyCaptureDialog(QWidget* parent) +SettingsKeyCaptureDialog::SettingsKeyCaptureDialog(QWidget *parent) { setWindowTitle(tr("Key Capture")); @@ -23,7 +23,7 @@ SettingsKeyCaptureDialog::SettingsKeyCaptureDialog(QWidget* parent) connect(button, &QPushButton::clicked, this, &QDialog::accept); } -void SettingsKeyCaptureDialog::keyReleaseEvent(QKeyEvent* event) +void SettingsKeyCaptureDialog::keyReleaseEvent(QKeyEvent *event) { KeyCaptured(Qt::Key(event->key())); accept(); diff --git a/lib/src/audioreceiver.c b/lib/src/audioreceiver.c index 5808e4c..9fec69d 100644 --- a/lib/src/audioreceiver.c +++ b/lib/src/audioreceiver.c @@ -23,7 +23,6 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_audio_receiver_init(ChiakiAudioReceiver *au return CHIAKI_ERR_SUCCESS; } - CHIAKI_EXPORT void chiaki_audio_receiver_fini(ChiakiAudioReceiver *audio_receiver) { #ifdef CHIAKI_LIB_ENABLE_OPUS @@ -32,7 +31,6 @@ CHIAKI_EXPORT void chiaki_audio_receiver_fini(ChiakiAudioReceiver *audio_receive chiaki_mutex_fini(&audio_receiver->mutex); } - CHIAKI_EXPORT void chiaki_audio_receiver_stream_info(ChiakiAudioReceiver *audio_receiver, ChiakiAudioHeader *audio_header) { chiaki_mutex_lock(&audio_receiver->mutex); @@ -79,15 +77,15 @@ CHIAKI_EXPORT void chiaki_audio_receiver_av_packet(ChiakiAudioReceiver *audio_re if(packet->data_size != (size_t)unit_size * (size_t)packet->units_in_frame_total) { CHIAKI_LOGE(audio_receiver->log, "Audio AV Packet size mismatch %#llx vs %#llx", - (unsigned long long)packet->data_size, - (unsigned long long)(unit_size * packet->units_in_frame_total)); + (unsigned long long)packet->data_size, + (unsigned long long)(unit_size * packet->units_in_frame_total)); return; } if(packet->frame_index > (1 << 15)) audio_receiver->frame_index_startup = false; - for(size_t i=0; i> 18) & 63; @@ -46,7 +45,7 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_base64_encode(const uint8_t *in, size_t in_ // if we have only two bytes available, then their encoding is // spread out over three chars - if((x+1) < in_size) + if((x + 1) < in_size) { if(result_index >= out_size) return CHIAKI_ERR_BUF_TOO_SMALL; @@ -55,7 +54,7 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_base64_encode(const uint8_t *in, size_t in_ // if we have all three bytes available, then their encoding is spread // out over four characters - if((x+2) < in_size) + if((x + 2) < in_size) { if(result_index >= out_size) return CHIAKI_ERR_BUF_TOO_SMALL; @@ -65,9 +64,9 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_base64_encode(const uint8_t *in, size_t in_ // create and add padding that is required if we did not have a multiple of 3 // number of characters available - if (pad_count > 0) + if(pad_count > 0) { - for (; pad_count < 3; pad_count++) + for(; pad_count < 3; pad_count++) { if(result_index >= out_size) return CHIAKI_ERR_BUF_TOO_SMALL; @@ -80,25 +79,22 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_base64_encode(const uint8_t *in, size_t in_ return CHIAKI_ERR_SUCCESS; } - - - #define WHITESPACE 64 -#define EQUALS 65 -#define INVALID 66 +#define EQUALS 65 +#define INVALID 66 static const unsigned char d[] = { - 66,66,66,66,66,66,66,66,66,66,64,66,66,66,66,66,66,66,66,66,66,66,66,66,66, - 66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,62,66,66,66,63,52,53, - 54,55,56,57,58,59,60,61,66,66,66,65,66,66,66, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, - 10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,66,66,66,66,66,66,26,27,28, - 29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,66,66, - 66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66, - 66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66, - 66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66, - 66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66, - 66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66,66, - 66,66,66,66,66,66 + 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 64, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, + 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 62, 66, 66, 66, 63, 52, 53, + 54, 55, 56, 57, 58, 59, 60, 61, 66, 66, 66, 65, 66, 66, 66, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, + 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 66, 66, 66, 66, 66, 66, 26, 27, 28, + 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 66, 66, + 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, + 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, + 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, + 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, + 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, + 66, 66, 66, 66, 66, 66 }; CHIAKI_EXPORT ChiakiErrorCode chiaki_base64_decode(const char *in, size_t in_size, uint8_t *out, size_t *out_size) @@ -108,17 +104,17 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_base64_decode(const char *in, size_t in_siz uint32_t buf = 0; size_t len = 0; - while (in < end) + while(in < end) { unsigned char c = d[(size_t)(*in++)]; switch(c) { case WHITESPACE: - continue; // skip whitespace + continue; // skip whitespace case INVALID: - return CHIAKI_ERR_INVALID_DATA; // invalid input - case EQUALS: // pad character, end of data + return CHIAKI_ERR_INVALID_DATA; // invalid input + case EQUALS: // pad character, end of data in = end; continue; default: @@ -132,7 +128,8 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_base64_decode(const char *in, size_t in_siz *(out++) = (unsigned char)((buf >> 16) & 0xff); *(out++) = (unsigned char)((buf >> 8) & 0xff); *(out++) = (unsigned char)(buf & 0xff); - buf = 0; iter = 0; + buf = 0; + iter = 0; } } } diff --git a/lib/src/common.c b/lib/src/common.c index 0dd2006..adb0fc6 100644 --- a/lib/src/common.c +++ b/lib/src/common.c @@ -1,14 +1,14 @@ // SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL #include -#include #include +#include #include +#include #include #include -#include #ifdef _WIN32 #include @@ -98,7 +98,7 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_lib_init() WORD wsa_version = MAKEWORD(2, 2); WSADATA wsa_data; int err = WSAStartup(wsa_version, &wsa_data); - if (err != 0) + if(err != 0) return CHIAKI_ERR_NETWORK; } #endif diff --git a/lib/src/congestioncontrol.c b/lib/src/congestioncontrol.c index e478410..8fb912b 100644 --- a/lib/src/congestioncontrol.c +++ b/lib/src/congestioncontrol.c @@ -25,7 +25,7 @@ static void *congestion_control_thread_func(void *user) packet.received = (uint16_t)received; packet.lost = (uint16_t)lost; CHIAKI_LOGV(control->takion->log, "Sending Congestion Control Packet, received: %u, lost: %u", - (unsigned int)packet.received, (unsigned int)packet.lost); + (unsigned int)packet.received, (unsigned int)packet.lost); chiaki_takion_send_congestion(control->takion, &packet); } diff --git a/lib/src/controller.c b/lib/src/controller.c index b347bb7..0e3a522 100644 --- a/lib/src/controller.c +++ b/lib/src/controller.c @@ -14,7 +14,7 @@ CHIAKI_EXPORT void chiaki_controller_state_set_idle(ChiakiControllerState *state state->right_x = 0; state->right_y = 0; state->touch_id_next = 0; - for(size_t i=0; itouches[i].id = -1; state->touches[i].x = 0; @@ -24,7 +24,7 @@ CHIAKI_EXPORT void chiaki_controller_state_set_idle(ChiakiControllerState *state CHIAKI_EXPORT int8_t chiaki_controller_state_start_touch(ChiakiControllerState *state, uint16_t x, uint16_t y) { - for(size_t i=0; itouches[i].id < 0) { @@ -40,7 +40,7 @@ CHIAKI_EXPORT int8_t chiaki_controller_state_start_touch(ChiakiControllerState * CHIAKI_EXPORT void chiaki_controller_state_stop_touch(ChiakiControllerState *state, uint8_t id) { - for(size_t i=0; itouches[i].id == id) { @@ -53,7 +53,7 @@ CHIAKI_EXPORT void chiaki_controller_state_stop_touch(ChiakiControllerState *sta CHIAKI_EXPORT void chiaki_controller_state_set_touch_pos(ChiakiControllerState *state, uint8_t id, uint16_t x, uint16_t y) { id &= TOUCH_ID_MASK; - for(size_t i=0; itouches[i].id == id) { @@ -64,8 +64,8 @@ CHIAKI_EXPORT void chiaki_controller_state_set_touch_pos(ChiakiControllerState * } } -#define MAX(a, b) ((a) > (b) ? (a) : (b)) -#define ABS(a) ((a) > 0 ? (a) : -(a)) +#define MAX(a, b) ((a) > (b) ? (a) : (b)) +#define ABS(a) ((a) > 0 ? (a) : -(a)) #define MAX_ABS(a, b) (ABS(a) > ABS(b) ? (a) : (b)) CHIAKI_EXPORT void chiaki_controller_state_or(ChiakiControllerState *out, ChiakiControllerState *a, ChiakiControllerState *b) @@ -79,7 +79,7 @@ CHIAKI_EXPORT void chiaki_controller_state_or(ChiakiControllerState *out, Chiaki out->right_y = MAX_ABS(a->right_y, b->right_y); out->touch_id_next = 0; - for(size_t i=0; itouches[i].id >= 0 ? &a->touches[i] : (b->touches[i].id >= 0 ? &b->touches[i] : NULL); if(!touch) diff --git a/lib/src/ctrl.c b/lib/src/ctrl.c index f92d12b..9dfc277 100644 --- a/lib/src/ctrl.c +++ b/lib/src/ctrl.c @@ -501,7 +501,6 @@ static void ctrl_enable_optional_features(ChiakiCtrl *ctrl) ctrl_message_send(ctrl, 0x36, pre_enable, 4); } - static void ctrl_message_received_session_id(ChiakiCtrl *ctrl, uint8_t *payload, size_t payload_size) { if(ctrl->session->ctrl_session_id_received) @@ -652,7 +651,7 @@ static void ctrl_message_received_keyboard_close(ChiakiCtrl *ctrl, uint8_t *payl chiaki_session_send_event(ctrl->session, &keyboard_event); } -static void ctrl_message_received_keyboard_text_change(ChiakiCtrl* ctrl, uint8_t* payload, size_t payload_size) +static void ctrl_message_received_keyboard_text_change(ChiakiCtrl *ctrl, uint8_t *payload, size_t payload_size) { assert(payload_size >= sizeof(CtrlKeyboardTextResponseMessage)); @@ -676,7 +675,8 @@ static void ctrl_message_received_keyboard_text_change(ChiakiCtrl* ctrl, uint8_t free(buffer); } -typedef struct ctrl_response_t { +typedef struct ctrl_response_t +{ bool server_type_valid; uint8_t rp_server_type[0x10]; bool success; @@ -742,7 +742,6 @@ static ChiakiErrorCode ctrl_connect(ChiakiCtrl *ctrl) if(err != CHIAKI_ERR_SUCCESS) CHIAKI_LOGE(session->log, "Failed to set ctrl socket to non-blocking: %s", chiaki_error_string(err)); - chiaki_mutex_unlock(&ctrl->notif_mutex); err = chiaki_stop_pipe_connect(&ctrl->notif_pipe, sock, sa, addr->ai_addrlen); chiaki_mutex_lock(&ctrl->notif_mutex); diff --git a/lib/src/ecdh.c b/lib/src/ecdh.c index 1a48b54..3277b60 100644 --- a/lib/src/ecdh.c +++ b/lib/src/ecdh.c @@ -79,7 +79,6 @@ CHIAKI_EXPORT void chiaki_ecdh_fini(ChiakiECDH *ecdh) #endif } - CHIAKI_EXPORT ChiakiErrorCode chiaki_ecdh_set_local_key(ChiakiECDH *ecdh, const uint8_t *private_key, size_t private_key_size, const uint8_t *public_key, size_t public_key_size) { #ifdef CHIAKI_LIB_ENABLE_MBEDTLS @@ -89,24 +88,19 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_ecdh_set_local_key(ChiakiECDH *ecdh, const // public int r = 0; - r = mbedtls_ecp_point_read_binary(&ecdh->ctx.grp, &ecdh->ctx.Q, - public_key, public_key_size); - if(r != 0 ){ + r = mbedtls_ecp_point_read_binary(&ecdh->ctx.grp, &ecdh->ctx.Q, public_key, public_key_size); + if(r != 0) return CHIAKI_ERR_UNKNOWN; - } // secret r = mbedtls_mpi_read_binary(&ecdh->ctx.d, private_key, private_key_size); - if(r != 0 ){ + if(r != 0) return CHIAKI_ERR_UNKNOWN; - } // regen key - r = mbedtls_ecdh_gen_public(&ecdh->ctx.grp, &ecdh->ctx.d, - &ecdh->ctx.Q, mbedtls_ctr_drbg_random, &ecdh->drbg); - if(r != 0 ){ + r = mbedtls_ecdh_gen_public(&ecdh->ctx.grp, &ecdh->ctx.d, &ecdh->ctx.Q, mbedtls_ctr_drbg_random, &ecdh->drbg); + if(r != 0) return CHIAKI_ERR_UNKNOWN; - } return CHIAKI_ERR_SUCCESS; #else diff --git a/lib/src/frameprocessor.c b/lib/src/frameprocessor.c index 058f6ac..c072a98 100644 --- a/lib/src/frameprocessor.c +++ b/lib/src/frameprocessor.c @@ -150,7 +150,7 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_frame_processor_put_unit(ChiakiFrameProcess CHIAKI_LOGE(frame_processor->log, "Packet's unit index is too high"); return CHIAKI_ERR_INVALID_DATA; } - + if(!packet->data_size) { CHIAKI_LOGW(frame_processor->log, "Unit is empty"); @@ -162,7 +162,7 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_frame_processor_put_unit(ChiakiFrameProcess CHIAKI_LOGW(frame_processor->log, "Unit is bigger than pre-calculated size!"); return CHIAKI_ERR_INVALID_DATA; } - + ChiakiFrameUnit *unit = frame_processor->unit_slots + packet->unit_index; if(unit->data_size) { diff --git a/lib/src/gkcrypt.c b/lib/src/gkcrypt.c index c99c5c2..e8dcb12 100644 --- a/lib/src/gkcrypt.c +++ b/lib/src/gkcrypt.c @@ -19,10 +19,8 @@ #include "utils.h" - #define KEY_BUF_CHUNK_SIZE 0x1000 - static ChiakiErrorCode gkcrypt_gen_key_iv(ChiakiGKCrypt *gkcrypt, uint8_t index, const uint8_t *handshake_key, const uint8_t *ecdh_secret); static void *gkcrypt_thread_func(void *user); @@ -110,7 +108,6 @@ CHIAKI_EXPORT void chiaki_gkcrypt_fini(ChiakiGKCrypt *gkcrypt) } } - static ChiakiErrorCode gkcrypt_gen_key_iv(ChiakiGKCrypt *gkcrypt, uint8_t index, const uint8_t *handshake_key, const uint8_t *ecdh_secret) { uint8_t data[3 + CHIAKI_HANDSHAKE_KEY_SIZE + 2]; @@ -123,37 +120,41 @@ static ChiakiErrorCode gkcrypt_gen_key_iv(ChiakiGKCrypt *gkcrypt, uint8_t index, uint8_t hmac[CHIAKI_GKCRYPT_BLOCK_SIZE*2]; size_t hmac_size = sizeof(hmac); - #ifdef CHIAKI_LIB_ENABLE_MBEDTLS +#ifdef CHIAKI_LIB_ENABLE_MBEDTLS mbedtls_md_context_t ctx; mbedtls_md_init(&ctx); - if(mbedtls_md_setup(&ctx, mbedtls_md_info_from_type(MBEDTLS_MD_SHA256) , 1) != 0){ + if(mbedtls_md_setup(&ctx, mbedtls_md_info_from_type(MBEDTLS_MD_SHA256), 1) != 0) + { mbedtls_md_free(&ctx); return CHIAKI_ERR_UNKNOWN; } - if(mbedtls_md_hmac_starts(&ctx, ecdh_secret, CHIAKI_ECDH_SECRET_SIZE) != 0){ + if(mbedtls_md_hmac_starts(&ctx, ecdh_secret, CHIAKI_ECDH_SECRET_SIZE) != 0) + { mbedtls_md_free(&ctx); return CHIAKI_ERR_UNKNOWN; } - if(mbedtls_md_hmac_update(&ctx, data, sizeof(data)) != 0){ + if(mbedtls_md_hmac_update(&ctx, data, sizeof(data)) != 0) + { mbedtls_md_free(&ctx); return CHIAKI_ERR_UNKNOWN; } - if(mbedtls_md_hmac_finish(&ctx, hmac) != 0){ + if(mbedtls_md_hmac_finish(&ctx, hmac) != 0) + { mbedtls_md_free(&ctx); return CHIAKI_ERR_UNKNOWN; } mbedtls_md_free(&ctx); - #else +#else if(!HMAC(EVP_sha256(), ecdh_secret, CHIAKI_ECDH_SECRET_SIZE, data, sizeof(data), hmac, (unsigned int *)&hmac_size)) return CHIAKI_ERR_UNKNOWN; - #endif +#endif assert(hmac_size == sizeof(hmac)); memcpy(gkcrypt->key_base, hmac, CHIAKI_GKCRYPT_BLOCK_SIZE); @@ -220,7 +221,8 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_gkcrypt_gen_key_stream(ChiakiGKCrypt *gkcry mbedtls_aes_context ctx; mbedtls_aes_init(&ctx); - if(mbedtls_aes_setkey_enc(&ctx, gkcrypt->key_base, 128) != 0){ + if(mbedtls_aes_setkey_enc(&ctx, gkcrypt->key_base, 128) != 0) + { mbedtls_aes_free(&ctx); return CHIAKI_ERR_UNKNOWN; } @@ -248,9 +250,11 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_gkcrypt_gen_key_stream(ChiakiGKCrypt *gkcry counter_add(cur, gkcrypt->iv, counter_offset++); #ifdef CHIAKI_LIB_ENABLE_MBEDTLS - for(int i=0; i #endif - CHIAKI_EXPORT void chiaki_http_header_free(ChiakiHttpHeader *header) { while(header) diff --git a/lib/src/packetstats.c b/lib/src/packetstats.c index c04adc8..c7da9d6 100644 --- a/lib/src/packetstats.c +++ b/lib/src/packetstats.c @@ -74,4 +74,3 @@ CHIAKI_EXPORT void chiaki_packet_stats_get(ChiakiPacketStats *stats, bool reset, reset_stats(stats); chiaki_mutex_unlock(&stats->mutex); } - diff --git a/lib/src/random.c b/lib/src/random.c index 974db60..158ce4d 100644 --- a/lib/src/random.c +++ b/lib/src/random.c @@ -28,10 +28,12 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_random_bytes_crypt(uint8_t *buf, size_t buf mbedtls_entropy_init(&entropy); mbedtls_ctr_drbg_set_prediction_resistance(&ctr_drbg, MBEDTLS_CTR_DRBG_PR_OFF); - if(mbedtls_ctr_drbg_seed(&ctr_drbg, mbedtls_entropy_func, &entropy, (const unsigned char *) "RANDOM_GEN", 10 ) != 0 ){ + if(mbedtls_ctr_drbg_seed(&ctr_drbg, mbedtls_entropy_func, &entropy, (const unsigned char *)"RANDOM_GEN", 10) != 0) + { return CHIAKI_ERR_UNKNOWN; } - if(mbedtls_ctr_drbg_random(&ctr_drbg, buf, buf_size) != 0){ + if(mbedtls_ctr_drbg_random(&ctr_drbg, buf, buf_size) != 0) + { return CHIAKI_ERR_UNKNOWN; } diff --git a/lib/src/regist.c b/lib/src/regist.c index 47c6ab1..af17942 100644 --- a/lib/src/regist.c +++ b/lib/src/regist.c @@ -160,7 +160,6 @@ static int request_header_format(char *buf, size_t buf_size, size_t payload_size return cur; } - CHIAKI_EXPORT ChiakiErrorCode chiaki_regist_request_payload_format(ChiakiTarget target, const uint8_t *ambassador, uint8_t *buf, size_t *buf_size, ChiakiRPCrypt *crypt, const char *psn_online_id, const uint8_t *psn_account_id, uint32_t pin) { size_t buf_size_val = *buf_size; diff --git a/lib/src/rpcrypt.c b/lib/src/rpcrypt.c index 197580e..30225c0 100644 --- a/lib/src/rpcrypt.c +++ b/lib/src/rpcrypt.c @@ -1488,7 +1488,7 @@ static ChiakiErrorCode bright_ambassador(ChiakiTarget target, uint8_t *bright, u 0x20, 0x98, 0xfd, 0x34, 0xca, 0x7a, 0x66, 0x20, 0x58, 0xd2, 0x36, 0x7f, 0x2b, 0xa7, 0xd1, 0xde, 0x6f, 0x36, 0xb4, 0xf2, 0x3b, 0x20, 0x5d, 0x02 - }; + }; if(target < CHIAKI_TARGET_PS4_10) return CHIAKI_ERR_INVALID_DATA; @@ -1865,7 +1865,6 @@ static const uint8_t *rpcrypt_hmac_key(ChiakiRPCrypt *rpcrypt) } } - #ifdef CHIAKI_LIB_ENABLE_MBEDTLS CHIAKI_EXPORT ChiakiErrorCode chiaki_rpcrypt_generate_iv(ChiakiRPCrypt *rpcrypt, uint8_t *iv, uint64_t counter) { @@ -1968,7 +1967,6 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_rpcrypt_generate_iv(ChiakiRPCrypt *rpcrypt, buf[CHIAKI_RPCRYPT_KEY_SIZE + 6] = (uint8_t)((counter >> 0x08) & 0xff); buf[CHIAKI_RPCRYPT_KEY_SIZE + 7] = (uint8_t)((counter >> 0x00) & 0xff); - uint8_t hmac[32]; unsigned int hmac_len = 0; if(!HMAC(EVP_sha256(), hmac_key, CHIAKI_RPCRYPT_KEY_SIZE, buf, sizeof(buf), hmac, &hmac_len)) diff --git a/lib/src/senkusha.c b/lib/src/senkusha.c index 1fa33d4..ebab5d7 100644 --- a/lib/src/senkusha.c +++ b/lib/src/senkusha.c @@ -645,7 +645,6 @@ static void senkusha_takion_data(ChiakiSenkusha *senkusha, ChiakiTakionMessageDa senkusha->state_finished = true; chiaki_cond_signal(&senkusha->state_cond); } - } chiaki_mutex_unlock(&senkusha->state_mutex); } @@ -878,4 +877,3 @@ static ChiakiErrorCode senkusha_send_data_wait_for_ack(ChiakiSenkusha *senkusha, return err; } - diff --git a/lib/src/session.c b/lib/src/session.c index a57ea1d..32cb646 100644 --- a/lib/src/session.c +++ b/lib/src/session.c @@ -540,10 +540,8 @@ quit: #undef QUIT } - - - -typedef struct session_response_t { +typedef struct session_response_t +{ uint32_t error_code; const char *nonce; const char *rp_version; @@ -652,7 +650,6 @@ static ChiakiErrorCode session_thread_request_session(ChiakiSession *session, Ch break; } - if(CHIAKI_SOCKET_IS_INVALID(session_sock)) { CHIAKI_LOGE(session->log, "Session request connect failed eventually."); diff --git a/lib/src/stoppipe.c b/lib/src/stoppipe.c index dd069ef..7ad2d2a 100644 --- a/lib/src/stoppipe.c +++ b/lib/src/stoppipe.c @@ -27,9 +27,8 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_stop_pipe_init(ChiakiStopPipe *stop_pipe) int addr_size = sizeof(stop_pipe->addr); stop_pipe->fd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); - if(stop_pipe->fd < 0){ + if(stop_pipe->fd < 0) return CHIAKI_ERR_UNKNOWN; - } stop_pipe->addr.sin_family = AF_INET; // bind to localhost stop_pipe->addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); diff --git a/lib/src/streamconnection.c b/lib/src/streamconnection.c index 28b22dc..2a7bb6f 100644 --- a/lib/src/streamconnection.c +++ b/lib/src/streamconnection.c @@ -56,7 +56,6 @@ static ChiakiErrorCode stream_connection_send_streaminfo_ack(ChiakiStreamConnect static void stream_connection_takion_av(ChiakiStreamConnection *stream_connection, ChiakiTakionAVPacket *packet); static ChiakiErrorCode stream_connection_send_heartbeat(ChiakiStreamConnection *stream_connection); - CHIAKI_EXPORT ChiakiErrorCode chiaki_stream_connection_init(ChiakiStreamConnection *stream_connection, ChiakiSession *session) { stream_connection->session = session; @@ -121,7 +120,6 @@ CHIAKI_EXPORT void chiaki_stream_connection_fini(ChiakiStreamConnection *stream_ chiaki_mutex_fini(&stream_connection->state_mutex); } - static bool state_finished_cond_check(void *user) { ChiakiStreamConnection *stream_connection = user; @@ -399,7 +397,6 @@ static void stream_connection_takion_data_protobuf(ChiakiStreamConnection *strea break; } chiaki_mutex_unlock(&stream_connection->state_mutex); - } static void stream_connection_takion_data_rumble(ChiakiStreamConnection *stream_connection, uint8_t *buf, size_t buf_size) @@ -640,7 +637,6 @@ static bool pb_decode_resolution(pb_istream_t *stream, const pb_field_t *field, return true; } - static void stream_connection_takion_data_expect_streaminfo(ChiakiStreamConnection *stream_connection, uint8_t *buf, size_t buf_size) { tkproto_TakionMessage msg; @@ -709,11 +705,9 @@ error: chiaki_cond_signal(&stream_connection->state_cond); } - - static bool chiaki_pb_encode_zero_encrypted_key(pb_ostream_t *stream, const pb_field_t *field, void *const *arg) { - if (!pb_encode_tag_for_field(stream, field)) + if(!pb_encode_tag_for_field(stream, field)) return false; uint8_t data[] = { 0, 0, 0, 0 }; return pb_encode_string(stream, data, sizeof(data)); @@ -867,7 +861,6 @@ static ChiakiErrorCode stream_connection_send_disconnect(ChiakiStreamConnection return err; } - static void stream_connection_takion_av(ChiakiStreamConnection *stream_connection, ChiakiTakionAVPacket *packet) { chiaki_gkcrypt_decrypt(stream_connection->gkcrypt_remote, packet->key_pos + CHIAKI_GKCRYPT_BLOCK_SIZE, packet->data, packet->data_size); @@ -878,7 +871,6 @@ static void stream_connection_takion_av(ChiakiStreamConnection *stream_connectio chiaki_audio_receiver_av_packet(stream_connection->audio_receiver, packet); } - static ChiakiErrorCode stream_connection_send_heartbeat(ChiakiStreamConnection *stream_connection) { tkproto_TakionMessage msg = { 0 }; diff --git a/lib/src/takion.c b/lib/src/takion.c index fe3e95b..c433977 100644 --- a/lib/src/takion.c +++ b/lib/src/takion.c @@ -107,7 +107,6 @@ typedef enum takion_chunk_type_t { TAKION_CHUNK_TYPE_COOKIE_ACK = 0xb, } TakionChunkType; - typedef struct takion_message_t { uint32_t tag; @@ -120,7 +119,6 @@ typedef struct takion_message_t uint8_t *payload; } TakionMessage; - typedef struct takion_message_payload_init_t { uint32_t tag; @@ -152,14 +150,12 @@ typedef struct uint16_t channel; } TakionDataPacketEntry; - typedef struct chiaki_takion_postponed_packet_t { uint8_t *buf; size_t buf_size; } ChiakiTakionPostponedPacket; - static void *takion_thread_func(void *user); static void takion_handle_packet(ChiakiTakion *takion, uint8_t *buf, size_t buf_size); static ChiakiErrorCode takion_handle_packet_mac(ChiakiTakion *takion, uint8_t base_type, uint8_t *buf, size_t buf_size); @@ -507,7 +503,7 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_takion_send_congestion(ChiakiTakion *takion return err; uint8_t buf[CHIAKI_TAKION_CONGESTION_PACKET_SIZE]; - chiaki_takion_format_congestion(buf, packet, key_pos); + chiaki_takion_format_congestion(buf, packet, key_pos); return chiaki_takion_send(takion, buf, sizeof(buf), key_pos); } @@ -603,7 +599,6 @@ static ChiakiErrorCode takion_handshake(ChiakiTakion *takion, uint32_t *seq_num_ CHIAKI_LOGI(takion->log, "Takion sent init"); - // INIT_ACK <- TakionMessagePayloadInitAck init_ack_payload; @@ -621,20 +616,17 @@ static ChiakiErrorCode takion_handshake(ChiakiTakion *takion, uint32_t *seq_num_ } CHIAKI_LOGI(takion->log, "Takion received init ack with remote tag %#x, outbound streams: %#x, inbound streams: %#x", - init_ack_payload.tag, init_ack_payload.outbound_streams, init_ack_payload.inbound_streams); + init_ack_payload.tag, init_ack_payload.outbound_streams, init_ack_payload.inbound_streams); takion->tag_remote = init_ack_payload.tag; *seq_num_remote_initial = takion->tag_remote; //init_ack_payload.initial_seq_num; - if(init_ack_payload.outbound_streams == 0 || init_ack_payload.inbound_streams == 0 - || init_ack_payload.outbound_streams > TAKION_INBOUND_STREAMS - || init_ack_payload.inbound_streams < TAKION_OUTBOUND_STREAMS) + if(init_ack_payload.outbound_streams == 0 || init_ack_payload.inbound_streams == 0 || init_ack_payload.outbound_streams > TAKION_INBOUND_STREAMS || init_ack_payload.inbound_streams < TAKION_OUTBOUND_STREAMS) { CHIAKI_LOGE(takion->log, "Takion min/max check failed"); return CHIAKI_ERR_INVALID_RESPONSE; } - // COOKIE -> err = takion_send_message_cookie(takion, init_ack_payload.cookie); @@ -780,7 +772,6 @@ beach: return NULL; } - static ChiakiErrorCode takion_recv(ChiakiTakion *takion, uint8_t *buf, size_t *buf_size, uint64_t timeout_ms) { ChiakiErrorCode err = chiaki_stop_pipe_select_single(&takion->stop_pipe, takion->sock, false, timeout_ms); @@ -805,7 +796,6 @@ static ChiakiErrorCode takion_recv(ChiakiTakion *takion, uint8_t *buf, size_t *b return CHIAKI_ERR_SUCCESS; } - static ChiakiErrorCode takion_handle_packet_mac(ChiakiTakion *takion, uint8_t base_type, uint8_t *buf, size_t buf_size) { if(!takion->gkcrypt_remote) @@ -866,7 +856,6 @@ static void takion_postpone_packet(ChiakiTakion *takion, uint8_t *buf, size_t bu packet->buf_size = buf_size; } - /** * @param buf ownership of this buf is taken. */ @@ -934,7 +923,6 @@ static void takion_handle_packet_message(ChiakiTakion *takion, uint8_t *buf, siz } } - static void takion_flush_data_queue(ChiakiTakion *takion) { uint64_t seq_num = 0; @@ -1104,7 +1092,6 @@ static ChiakiErrorCode takion_parse_message(ChiakiTakion *takion, uint8_t *buf, return CHIAKI_ERR_SUCCESS; } - static ChiakiErrorCode takion_send_message_init(ChiakiTakion *takion, TakionMessagePayloadInit *payload) { uint8_t message[1 + TAKION_MESSAGE_HEADER_SIZE + 0x10]; @@ -1121,8 +1108,6 @@ static ChiakiErrorCode takion_send_message_init(ChiakiTakion *takion, TakionMess return chiaki_takion_send_raw(takion, message, sizeof(message)); } - - static ChiakiErrorCode takion_send_message_cookie(ChiakiTakion *takion, uint8_t *cookie) { uint8_t message[1 + TAKION_MESSAGE_HEADER_SIZE + TAKION_COOKIE_SIZE]; @@ -1132,8 +1117,6 @@ static ChiakiErrorCode takion_send_message_cookie(ChiakiTakion *takion, uint8_t return chiaki_takion_send_raw(takion, message, sizeof(message)); } - - static ChiakiErrorCode takion_recv_message_init_ack(ChiakiTakion *takion, TakionMessagePayloadInitAck *payload) { uint8_t message[1 + TAKION_MESSAGE_HEADER_SIZE + 0x10 + TAKION_COOKIE_SIZE]; @@ -1181,7 +1164,6 @@ static ChiakiErrorCode takion_recv_message_init_ack(ChiakiTakion *takion, Takion return CHIAKI_ERR_SUCCESS; } - static ChiakiErrorCode takion_recv_message_cookie_ack(ChiakiTakion *takion) { uint8_t message[1 + TAKION_MESSAGE_HEADER_SIZE]; @@ -1221,7 +1203,6 @@ static ChiakiErrorCode takion_recv_message_cookie_ack(ChiakiTakion *takion) return CHIAKI_ERR_SUCCESS; } - static void takion_handle_packet_av(ChiakiTakion *takion, uint8_t base_type, uint8_t *buf, size_t buf_size) { // HHIxIIx @@ -1453,5 +1434,4 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_takion_v7_av_packet_parse(ChiakiTakionAVPac packet->data_size = buf_size; return CHIAKI_ERR_SUCCESS; - } diff --git a/lib/src/thread.c b/lib/src/thread.c index 1948311..0539e16 100644 --- a/lib/src/thread.c +++ b/lib/src/thread.c @@ -23,7 +23,8 @@ static DWORD WINAPI win32_thread_func(LPVOID param) #endif #ifdef __SWITCH__ -int64_t get_thread_limit(){ +int64_t get_thread_limit() +{ uint64_t resource_limit_handle_value = INVALID_HANDLE; svcGetInfo(&resource_limit_handle_value, InfoType_ResourceLimit, INVALID_HANDLE, 0); int64_t thread_cur_value = 0, thread_lim_value = 0; @@ -45,9 +46,8 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_thread_create(ChiakiThread *thread, ChiakiT return CHIAKI_ERR_THREAD; #else #ifdef __SWITCH__ - if(get_thread_limit() <= 1){ + if(get_thread_limit() <= 1) return CHIAKI_ERR_THREAD; - } #endif int r = pthread_create(&thread->thread, NULL, func, arg); if(r != 0) @@ -90,13 +90,13 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_thread_set_name(ChiakiThread *thread, const if(r != 0) return CHIAKI_ERR_THREAD; #else - (void)thread; (void)name; + (void)thread; + (void)name; #endif #endif return CHIAKI_ERR_SUCCESS; } - CHIAKI_EXPORT ChiakiErrorCode chiaki_mutex_init(ChiakiMutex *mutex, bool rec) { #if _WIN32 @@ -172,9 +172,6 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_mutex_unlock(ChiakiMutex *mutex) return CHIAKI_ERR_SUCCESS; } - - - CHIAKI_EXPORT ChiakiErrorCode chiaki_cond_init(ChiakiCond *cond) { #if _WIN32 @@ -214,8 +211,6 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_cond_fini(ChiakiCond *cond) return CHIAKI_ERR_SUCCESS; } - - CHIAKI_EXPORT ChiakiErrorCode chiaki_cond_wait(ChiakiCond *cond, ChiakiMutex *mutex) { #if _WIN32 @@ -323,7 +318,6 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_cond_timedwait_pred(ChiakiCond *cond, Chiak #endif } return CHIAKI_ERR_SUCCESS; - } CHIAKI_EXPORT ChiakiErrorCode chiaki_cond_signal(ChiakiCond *cond) @@ -350,9 +344,6 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_cond_broadcast(ChiakiCond *cond) return CHIAKI_ERR_SUCCESS; } - - - CHIAKI_EXPORT ChiakiErrorCode chiaki_bool_pred_cond_init(ChiakiBoolPredCond *cond) { cond->pred = false; @@ -384,7 +375,6 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_bool_pred_cond_fini(ChiakiBoolPredCond *con return CHIAKI_ERR_SUCCESS; } - CHIAKI_EXPORT ChiakiErrorCode chiaki_bool_pred_cond_lock(ChiakiBoolPredCond *cond) { return chiaki_mutex_lock(&cond->mutex); diff --git a/switch/src/discoverymanager.cpp b/switch/src/discoverymanager.cpp index 7f71578..e9abe5f 100644 --- a/switch/src/discoverymanager.cpp +++ b/switch/src/discoverymanager.cpp @@ -16,9 +16,9 @@ #define HOSTS_MAX 16 #define DROP_PINGS 3 -static void Discovery(ChiakiDiscoveryHost * discovered_hosts, size_t hosts_count, void * user) +static void Discovery(ChiakiDiscoveryHost *discovered_hosts, size_t hosts_count, void *user) { - DiscoveryManager * dm = (DiscoveryManager *)user; + DiscoveryManager *dm = (DiscoveryManager *)user; for(size_t i = 0; i < hosts_count; i++) { dm->DiscoveryCB(discovered_hosts + i); @@ -105,7 +105,7 @@ uint32_t DiscoveryManager::GetIPv4BroadcastAddr() #endif } -int DiscoveryManager::Send(struct sockaddr * host_addr, size_t host_addr_len) +int DiscoveryManager::Send(struct sockaddr *host_addr, size_t host_addr_len) { if(!host_addr) { @@ -120,9 +120,9 @@ int DiscoveryManager::Send(struct sockaddr * host_addr, size_t host_addr_len) return 0; } -int DiscoveryManager::Send(const char * discover_ip_dest) +int DiscoveryManager::Send(const char *discover_ip_dest) { - struct addrinfo * host_addrinfos; + struct addrinfo *host_addrinfos; int r = getaddrinfo(discover_ip_dest, NULL, NULL, &host_addrinfos); if(r != 0) { @@ -130,10 +130,10 @@ int DiscoveryManager::Send(const char * discover_ip_dest) return 1; } - struct sockaddr * host_addr = nullptr; + struct sockaddr *host_addr = nullptr; socklen_t host_addr_len = 0; - for(struct addrinfo * ai = host_addrinfos; ai; ai = ai->ai_next) + for(struct addrinfo *ai = host_addrinfos; ai; ai = ai->ai_next) { if(ai->ai_protocol != IPPROTO_UDP) continue; @@ -170,13 +170,13 @@ int DiscoveryManager::Send() return DiscoveryManager::Send(this->host_addr, this->host_addr_len); } -void DiscoveryManager::DiscoveryCB(ChiakiDiscoveryHost * discovered_host) +void DiscoveryManager::DiscoveryCB(ChiakiDiscoveryHost *discovered_host) { // the user ptr is passed as // chiaki_discovery_thread_start arg std::string key = discovered_host->host_name; - Host * host = this->settings->GetOrCreateHost(&key); + Host *host = this->settings->GetOrCreateHost(&key); CHIAKI_LOGI(this->log, "--"); CHIAKI_LOGI(this->log, "Discovered Host:"); diff --git a/switch/src/gui.cpp b/switch/src/gui.cpp index e49852b..d784026 100644 --- a/switch/src/gui.cpp +++ b/switch/src/gui.cpp @@ -100,7 +100,7 @@ void HostInterface::Register(Host *host, std::function success_cb) brls::Dialog *peprpc = new brls::Dialog("Please enter your PlayStation registration PIN code"); brls::GenericEvent::Callback cb_peprpc = [host, io, peprpc](brls::View *view) { bool pin_provided = false; - char pin_input[9] = {0}; + char pin_input[9] = { 0 }; std::string error_message; // use callback to ensure that the message is showed on screen @@ -330,7 +330,7 @@ bool MainApplication::BuildConfigurationMenu(brls::List *ls, Host *host) brls::ListItem *psn_account_id = new brls::ListItem("PSN Account ID", "PS5 or PS4 v7.0 and greater (base64 account_id)"); psn_account_id->setValue(psn_account_id_string.c_str()); auto psn_account_id_cb = [this, host, psn_account_id](brls::View *view) { - char account_id[CHIAKI_PSN_ACCOUNT_ID_SIZE * 2] = {0}; + char account_id[CHIAKI_PSN_ACCOUNT_ID_SIZE * 2] = { 0 }; bool input = this->io->ReadUserKeyboard(account_id, sizeof(account_id)); if(input) { @@ -349,7 +349,7 @@ bool MainApplication::BuildConfigurationMenu(brls::List *ls, Host *host) brls::ListItem *psn_online_id = new brls::ListItem("PSN Online ID"); psn_online_id->setValue(psn_online_id_string.c_str()); auto psn_online_id_cb = [this, host, psn_online_id](brls::View *view) { - char online_id[256] = {0}; + char online_id[256] = { 0 }; bool input = this->io->ReadUserKeyboard(online_id, sizeof(online_id)); if(input) { @@ -380,7 +380,7 @@ bool MainApplication::BuildConfigurationMenu(brls::List *ls, Host *host) } brls::SelectListItem *resolution = new brls::SelectListItem( - "Resolution", {"720p", "540p", "360p"}, value); + "Resolution", { "720p", "540p", "360p" }, value); auto resolution_cb = [this, host](int result) { ChiakiVideoResolutionPreset value = CHIAKI_VIDEO_RESOLUTION_PRESET_720p; @@ -414,7 +414,7 @@ bool MainApplication::BuildConfigurationMenu(brls::List *ls, Host *host) } brls::SelectListItem *fps = new brls::SelectListItem( - "FPS", {"60", "30"}, value); + "FPS", { "60", "30" }, value); auto fps_cb = [this, host](int result) { ChiakiVideoFPSPreset value = CHIAKI_VIDEO_FPS_PRESET_60; @@ -467,7 +467,7 @@ void MainApplication::BuildAddHostConfigurationMenu(brls::List *add_host) brls::ListItem *display_name = new brls::ListItem("Display name"); auto display_name_cb = [this, display_name](brls::View *view) { - char name[16] = {0}; + char name[16] = { 0 }; bool input = this->io->ReadUserKeyboard(name, sizeof(name)); if(input) { @@ -482,7 +482,7 @@ void MainApplication::BuildAddHostConfigurationMenu(brls::List *add_host) brls::ListItem *address = new brls::ListItem("Remote IP/name"); auto address_cb = [this, address](brls::View *view) { - char addr[256] = {0}; + char addr[256] = { 0 }; bool input = this->io->ReadUserKeyboard(addr, sizeof(addr)); if(input) { @@ -500,7 +500,7 @@ void MainApplication::BuildAddHostConfigurationMenu(brls::List *add_host) // brls::ListItem* port = new brls::ListItem("Remote stream port", "udp 9296"); // brls::ListItem* port = new brls::ListItem("Remote Senkusha port", "udp 9297"); brls::SelectListItem *ps_version = new brls::SelectListItem("PlayStation Version", - {"PS5", "PS4 > 8", "7 < PS4 < 8", "PS4 < 7"}); + { "PS5", "PS4 > 8", "7 < PS4 < 8", "PS4 < 7" }); auto ps_version_cb = [this, ps_version](int result) { switch(result) { diff --git a/switch/src/io.cpp b/switch/src/io.cpp index d7c28f7..9f6ab56 100644 --- a/switch/src/io.cpp +++ b/switch/src/io.cpp @@ -646,7 +646,7 @@ bool IO::InitOpenGlTextures() D(glGenTextures(PLANES_COUNT, this->tex)); D(glGenBuffers(PLANES_COUNT, this->pbo)); - uint8_t uv_default[] = {0x7f, 0x7f}; + uint8_t uv_default[] = { 0x7f, 0x7f }; for(int i = 0; i < PLANES_COUNT; i++) { D(glBindTexture(GL_TEXTURE_2D, this->tex[i])); @@ -659,7 +659,7 @@ bool IO::InitOpenGlTextures() D(glUseProgram(this->prog)); // bind only as many planes as we need - const char *plane_names[] = {"plane1", "plane2", "plane3"}; + const char *plane_names[] = { "plane1", "plane2", "plane3" }; for(int i = 0; i < PLANES_COUNT; i++) D(glUniform1i(glGetUniformLocation(this->prog, plane_names[i]), i)); diff --git a/switch/src/main.cpp b/switch/src/main.cpp index c12d413..84f9210 100644 --- a/switch/src/main.cpp +++ b/switch/src/main.cpp @@ -111,11 +111,11 @@ extern "C" void userAppExit() } #endif // __SWITCH__ -int main(int argc, char * argv[]) +int main(int argc, char *argv[]) { // load chiaki lib - Settings * settings = Settings::GetInstance(); - ChiakiLog * log = settings->GetLogger(); + Settings *settings = Settings::GetInstance(); + ChiakiLog *log = settings->GetLogger(); CHIAKI_LOGI(log, "Loading chaki lib"); diff --git a/switch/src/settings.cpp b/switch/src/settings.cpp index f0eb3e3..f5edf1f 100644 --- a/switch/src/settings.cpp +++ b/switch/src/settings.cpp @@ -13,7 +13,7 @@ Settings::Settings() #endif } -Settings::ConfigurationItem Settings::ParseLine(std::string * line, std::string * value) +Settings::ConfigurationItem Settings::ParseLine(std::string *line, std::string *value) { Settings::ConfigurationItem ci; std::smatch m; @@ -35,9 +35,9 @@ size_t Settings::GetB64encodeSize(size_t in) return ((4 * in / 3) + 3) & ~3; } -Settings * Settings::instance = nullptr; +Settings *Settings::instance = nullptr; -Settings * Settings::GetInstance() +Settings *Settings::GetInstance() { if(instance == nullptr) { @@ -47,17 +47,17 @@ Settings * Settings::GetInstance() return instance; } -ChiakiLog * Settings::GetLogger() +ChiakiLog *Settings::GetLogger() { return &this->log; } -std::map * Settings::GetHostsMap() +std::map *Settings::GetHostsMap() { return &this->hosts; } -Host * Settings::GetOrCreateHost(std::string * host_name) +Host *Settings::GetOrCreateHost(std::string *host_name) { bool created = false; // update of create Host instance @@ -69,7 +69,7 @@ Host * Settings::GetOrCreateHost(std::string * host_name) created = true; } - Host * host = &(this->hosts.at(*host_name)); + Host *host = &(this->hosts.at(*host_name)); if(created) { // copy default settings @@ -90,7 +90,7 @@ void Settings::ParseFile() std::string line; std::string value; bool rp_key_b = false, rp_regist_key_b = false, rp_key_type_b = false; - Host * current_host = nullptr; + Host *current_host = nullptr; if(config_file.is_open()) { CHIAKI_LOGV(&this->log, "Config file opened"); @@ -102,63 +102,63 @@ void Settings::ParseFile() ci = this->ParseLine(&line, &value); switch(ci) { - // got to next line - case UNKNOWN: - CHIAKI_LOGV(&this->log, "UNKNOWN config"); - break; - case HOST_NAME: - CHIAKI_LOGV(&this->log, "HOST_NAME %s", value.c_str()); - // current host is in context - current_host = this->GetOrCreateHost(&value); - // all following case will edit the current_host config + // got to next line + case UNKNOWN: + CHIAKI_LOGV(&this->log, "UNKNOWN config"); + break; + case HOST_NAME: + CHIAKI_LOGV(&this->log, "HOST_NAME %s", value.c_str()); + // current host is in context + current_host = this->GetOrCreateHost(&value); + // all following case will edit the current_host config - rp_key_b = false; - rp_regist_key_b = false; - rp_key_type_b = false; - break; - case HOST_ADDR: - CHIAKI_LOGV(&this->log, "HOST_ADDR %s", value.c_str()); - if(current_host != nullptr) - current_host->host_addr = value; - break; - case PSN_ONLINE_ID: - CHIAKI_LOGV(&this->log, "PSN_ONLINE_ID %s", value.c_str()); - // current_host == nullptr - // means we are in global ini section - // update default setting - this->SetPSNOnlineID(current_host, value); - break; - case PSN_ACCOUNT_ID: - CHIAKI_LOGV(&this->log, "PSN_ACCOUNT_ID %s", value.c_str()); - this->SetPSNAccountID(current_host, value); - break; - case RP_KEY: - CHIAKI_LOGV(&this->log, "RP_KEY %s", value.c_str()); - if(current_host != nullptr) - rp_key_b = this->SetHostRPKey(current_host, value); - break; - case RP_KEY_TYPE: - CHIAKI_LOGV(&this->log, "RP_KEY_TYPE %s", value.c_str()); - if(current_host != nullptr) - // TODO Check possible rp_type values - rp_key_type_b = this->SetHostRPKeyType(current_host, value); - break; - case RP_REGIST_KEY: - CHIAKI_LOGV(&this->log, "RP_REGIST_KEY %s", value.c_str()); - if(current_host != nullptr) - rp_regist_key_b = this->SetHostRPRegistKey(current_host, value); - break; - case VIDEO_RESOLUTION: - this->SetVideoResolution(current_host, value); - break; - case VIDEO_FPS: - this->SetVideoFPS(current_host, value); - break; - case TARGET: - CHIAKI_LOGV(&this->log, "TARGET %s", value.c_str()); - if(current_host != nullptr) - this->SetChiakiTarget(current_host, value); - break; + rp_key_b = false; + rp_regist_key_b = false; + rp_key_type_b = false; + break; + case HOST_ADDR: + CHIAKI_LOGV(&this->log, "HOST_ADDR %s", value.c_str()); + if(current_host != nullptr) + current_host->host_addr = value; + break; + case PSN_ONLINE_ID: + CHIAKI_LOGV(&this->log, "PSN_ONLINE_ID %s", value.c_str()); + // current_host == nullptr + // means we are in global ini section + // update default setting + this->SetPSNOnlineID(current_host, value); + break; + case PSN_ACCOUNT_ID: + CHIAKI_LOGV(&this->log, "PSN_ACCOUNT_ID %s", value.c_str()); + this->SetPSNAccountID(current_host, value); + break; + case RP_KEY: + CHIAKI_LOGV(&this->log, "RP_KEY %s", value.c_str()); + if(current_host != nullptr) + rp_key_b = this->SetHostRPKey(current_host, value); + break; + case RP_KEY_TYPE: + CHIAKI_LOGV(&this->log, "RP_KEY_TYPE %s", value.c_str()); + if(current_host != nullptr) + // TODO Check possible rp_type values + rp_key_type_b = this->SetHostRPKeyType(current_host, value); + break; + case RP_REGIST_KEY: + CHIAKI_LOGV(&this->log, "RP_REGIST_KEY %s", value.c_str()); + if(current_host != nullptr) + rp_regist_key_b = this->SetHostRPRegistKey(current_host, value); + break; + case VIDEO_RESOLUTION: + this->SetVideoResolution(current_host, value); + break; + case VIDEO_FPS: + this->SetVideoFPS(current_host, value); + break; + case TARGET: + CHIAKI_LOGV(&this->log, "TARGET %s", value.c_str()); + if(current_host != nullptr) + this->SetChiakiTarget(current_host, value); + break; } // ci switch if(rp_key_b && rp_regist_key_b && rp_key_type_b) // the current host contains rp key data @@ -231,7 +231,7 @@ int Settings::WriteFile() if(it->second.rp_key_data || it->second.registered) { - char rp_key_type[33] = {0}; + char rp_key_type[33] = { 0 }; snprintf(rp_key_type, sizeof(rp_key_type), "%d", it->second.rp_key_type); // save registered rp key for auto login config_file << "rp_key = \"" << this->GetHostRPKey(&it->second) << "\"\n" @@ -250,14 +250,14 @@ std::string Settings::ResolutionPresetToString(ChiakiVideoResolutionPreset resol { switch(resolution) { - case CHIAKI_VIDEO_RESOLUTION_PRESET_360p: - return "360p"; - case CHIAKI_VIDEO_RESOLUTION_PRESET_540p: - return "540p"; - case CHIAKI_VIDEO_RESOLUTION_PRESET_720p: - return "720p"; - case CHIAKI_VIDEO_RESOLUTION_PRESET_1080p: - return "1080p"; + case CHIAKI_VIDEO_RESOLUTION_PRESET_360p: + return "360p"; + case CHIAKI_VIDEO_RESOLUTION_PRESET_540p: + return "540p"; + case CHIAKI_VIDEO_RESOLUTION_PRESET_720p: + return "720p"; + case CHIAKI_VIDEO_RESOLUTION_PRESET_1080p: + return "1080p"; } return "UNKNOWN"; } @@ -266,14 +266,14 @@ int Settings::ResolutionPresetToInt(ChiakiVideoResolutionPreset resolution) { switch(resolution) { - case CHIAKI_VIDEO_RESOLUTION_PRESET_360p: - return 360; - case CHIAKI_VIDEO_RESOLUTION_PRESET_540p: - return 540; - case CHIAKI_VIDEO_RESOLUTION_PRESET_720p: - return 720; - case CHIAKI_VIDEO_RESOLUTION_PRESET_1080p: - return 1080; + case CHIAKI_VIDEO_RESOLUTION_PRESET_360p: + return 360; + case CHIAKI_VIDEO_RESOLUTION_PRESET_540p: + return 540; + case CHIAKI_VIDEO_RESOLUTION_PRESET_720p: + return 720; + case CHIAKI_VIDEO_RESOLUTION_PRESET_1080p: + return 1080; } return 0; } @@ -300,10 +300,10 @@ std::string Settings::FPSPresetToString(ChiakiVideoFPSPreset fps) { switch(fps) { - case CHIAKI_VIDEO_FPS_PRESET_30: - return "30"; - case CHIAKI_VIDEO_FPS_PRESET_60: - return "60"; + case CHIAKI_VIDEO_FPS_PRESET_30: + return "30"; + case CHIAKI_VIDEO_FPS_PRESET_60: + return "60"; } return "UNKNOWN"; } @@ -312,10 +312,10 @@ int Settings::FPSPresetToInt(ChiakiVideoFPSPreset fps) { switch(fps) { - case CHIAKI_VIDEO_FPS_PRESET_30: - return 30; - case CHIAKI_VIDEO_FPS_PRESET_60: - return 60; + case CHIAKI_VIDEO_FPS_PRESET_30: + return 30; + case CHIAKI_VIDEO_FPS_PRESET_60: + return 60; } return 0; } @@ -334,7 +334,7 @@ ChiakiVideoFPSPreset Settings::StringToFPSPreset(std::string value) return CHIAKI_VIDEO_FPS_PRESET_30; } -std::string Settings::GetHostName(Host * host) +std::string Settings::GetHostName(Host *host) { if(host != nullptr) return host->GetHostName(); @@ -343,7 +343,7 @@ std::string Settings::GetHostName(Host * host) return ""; } -std::string Settings::GetHostAddr(Host * host) +std::string Settings::GetHostAddr(Host *host) { if(host != nullptr) return host->GetHostAddr(); @@ -352,7 +352,7 @@ std::string Settings::GetHostAddr(Host * host) return ""; } -std::string Settings::GetPSNOnlineID(Host * host) +std::string Settings::GetPSNOnlineID(Host *host) { if(host == nullptr || host->psn_online_id.length() == 0) return this->global_psn_online_id; @@ -360,7 +360,7 @@ std::string Settings::GetPSNOnlineID(Host * host) return host->psn_online_id; } -void Settings::SetPSNOnlineID(Host * host, std::string psn_online_id) +void Settings::SetPSNOnlineID(Host *host, std::string psn_online_id) { if(host == nullptr) this->global_psn_online_id = psn_online_id; @@ -368,7 +368,7 @@ void Settings::SetPSNOnlineID(Host * host, std::string psn_online_id) host->psn_online_id = psn_online_id; } -std::string Settings::GetPSNAccountID(Host * host) +std::string Settings::GetPSNAccountID(Host *host) { if(host == nullptr || host->psn_account_id.length() == 0) return this->global_psn_account_id; @@ -376,7 +376,7 @@ std::string Settings::GetPSNAccountID(Host * host) return host->psn_account_id; } -void Settings::SetPSNAccountID(Host * host, std::string psn_account_id) +void Settings::SetPSNAccountID(Host *host, std::string psn_account_id) { if(host == nullptr) this->global_psn_account_id = psn_account_id; @@ -384,7 +384,7 @@ void Settings::SetPSNAccountID(Host * host, std::string psn_account_id) host->psn_account_id = psn_account_id; } -ChiakiVideoResolutionPreset Settings::GetVideoResolution(Host * host) +ChiakiVideoResolutionPreset Settings::GetVideoResolution(Host *host) { if(host == nullptr) return this->global_video_resolution; @@ -392,7 +392,7 @@ ChiakiVideoResolutionPreset Settings::GetVideoResolution(Host * host) return host->video_resolution; } -void Settings::SetVideoResolution(Host * host, ChiakiVideoResolutionPreset value) +void Settings::SetVideoResolution(Host *host, ChiakiVideoResolutionPreset value) { if(host == nullptr) this->global_video_resolution = value; @@ -400,13 +400,13 @@ void Settings::SetVideoResolution(Host * host, ChiakiVideoResolutionPreset value host->video_resolution = value; } -void Settings::SetVideoResolution(Host * host, std::string value) +void Settings::SetVideoResolution(Host *host, std::string value) { ChiakiVideoResolutionPreset p = StringToResolutionPreset(value); this->SetVideoResolution(host, p); } -ChiakiVideoFPSPreset Settings::GetVideoFPS(Host * host) +ChiakiVideoFPSPreset Settings::GetVideoFPS(Host *host) { if(host == nullptr) return this->global_video_fps; @@ -414,7 +414,7 @@ ChiakiVideoFPSPreset Settings::GetVideoFPS(Host * host) return host->video_fps; } -void Settings::SetVideoFPS(Host * host, ChiakiVideoFPSPreset value) +void Settings::SetVideoFPS(Host *host, ChiakiVideoFPSPreset value) { if(host == nullptr) this->global_video_fps = value; @@ -422,18 +422,18 @@ void Settings::SetVideoFPS(Host * host, ChiakiVideoFPSPreset value) host->video_fps = value; } -void Settings::SetVideoFPS(Host * host, std::string value) +void Settings::SetVideoFPS(Host *host, std::string value) { ChiakiVideoFPSPreset p = StringToFPSPreset(value); this->SetVideoFPS(host, p); } -ChiakiTarget Settings::GetChiakiTarget(Host * host) +ChiakiTarget Settings::GetChiakiTarget(Host *host) { return host->GetChiakiTarget(); } -bool Settings::SetChiakiTarget(Host * host, ChiakiTarget target) +bool Settings::SetChiakiTarget(Host *host, ChiakiTarget target) { if(host != nullptr) { @@ -447,20 +447,20 @@ bool Settings::SetChiakiTarget(Host * host, ChiakiTarget target) } } -bool Settings::SetChiakiTarget(Host * host, std::string value) +bool Settings::SetChiakiTarget(Host *host, std::string value) { // TODO Check possible target values return this->SetChiakiTarget(host, static_cast(std::atoi(value.c_str()))); } -std::string Settings::GetHostRPKey(Host * host) +std::string Settings::GetHostRPKey(Host *host) { if(host != nullptr) { if(host->rp_key_data || host->registered) { size_t rp_key_b64_sz = this->GetB64encodeSize(0x10); - char rp_key_b64[rp_key_b64_sz + 1] = {0}; + char rp_key_b64[rp_key_b64_sz + 1] = { 0 }; ChiakiErrorCode err; err = chiaki_base64_encode( host->rp_key, 0x10, @@ -478,7 +478,7 @@ std::string Settings::GetHostRPKey(Host * host) return ""; } -bool Settings::SetHostRPKey(Host * host, std::string rp_key_b64) +bool Settings::SetHostRPKey(Host *host, std::string rp_key_b64) { if(host != nullptr) { @@ -497,14 +497,14 @@ bool Settings::SetHostRPKey(Host * host, std::string rp_key_b64) return false; } -std::string Settings::GetHostRPRegistKey(Host * host) +std::string Settings::GetHostRPRegistKey(Host *host) { if(host != nullptr) { if(host->rp_key_data || host->registered) { size_t rp_regist_key_b64_sz = this->GetB64encodeSize(CHIAKI_SESSION_AUTH_SIZE); - char rp_regist_key_b64[rp_regist_key_b64_sz + 1] = {0}; + char rp_regist_key_b64[rp_regist_key_b64_sz + 1] = { 0 }; ChiakiErrorCode err; err = chiaki_base64_encode( (uint8_t *)host->rp_regist_key, CHIAKI_SESSION_AUTH_SIZE, @@ -522,7 +522,7 @@ std::string Settings::GetHostRPRegistKey(Host * host) return ""; } -bool Settings::SetHostRPRegistKey(Host * host, std::string rp_regist_key_b64) +bool Settings::SetHostRPRegistKey(Host *host, std::string rp_regist_key_b64) { if(host != nullptr) { @@ -541,7 +541,7 @@ bool Settings::SetHostRPRegistKey(Host * host, std::string rp_regist_key_b64) return false; } -int Settings::GetHostRPKeyType(Host * host) +int Settings::GetHostRPKeyType(Host *host) { if(host != nullptr) return host->rp_key_type; @@ -550,7 +550,7 @@ int Settings::GetHostRPKeyType(Host * host) return 0; } -bool Settings::SetHostRPKeyType(Host * host, std::string value) +bool Settings::SetHostRPKeyType(Host *host, std::string value) { if(host != nullptr) { @@ -562,7 +562,7 @@ bool Settings::SetHostRPKeyType(Host * host, std::string value) } #ifdef CHIAKI_ENABLE_SWITCH_OVERCLOCK -int Settings::GetCPUOverclock(Host * host) +int Settings::GetCPUOverclock(Host *host) { if(host == nullptr) return this->global_cpu_overclock; @@ -570,7 +570,7 @@ int Settings::GetCPUOverclock(Host * host) return host->cpu_overclock; } -void Settings::SetCPUOverclock(Host * host, int value) +void Settings::SetCPUOverclock(Host *host, int value) { int oc = OC_1326; if(value > OC_1580) @@ -592,11 +592,9 @@ void Settings::SetCPUOverclock(Host * host, int value) host->cpu_overclock = oc; } -void Settings::SetCPUOverclock(Host * host, std::string value) +void Settings::SetCPUOverclock(Host *host, std::string value) { int v = atoi(value.c_str()); this->SetCPUOverclock(host, v); } #endif - - From da051803f59dab6aa31bef0a2f4f22eaf1679402 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Fri, 1 Jan 2021 21:19:52 +0100 Subject: [PATCH 142/237] Fall back to H264 on Pi --- gui/src/streamsession.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/gui/src/streamsession.cpp b/gui/src/streamsession.cpp index b1cbb47..01008a8 100644 --- a/gui/src/streamsession.cpp +++ b/gui/src/streamsession.cpp @@ -108,6 +108,14 @@ StreamSession::StreamSession(const StreamSessionConnectInfo &connect_info, QObje chiaki_connect_info.video_profile = connect_info.video_profile; chiaki_connect_info.video_profile_auto_downgrade = true; +#if CHIAKI_LIB_ENABLE_PI_DECODER + if(connect_info.decoder == Decoder::Pi && chiaki_connect_info.video_profile.codec != CHIAKI_CODEC_H264) + { + CHIAKI_LOGW(GetChiakiLog(), "A codec other than H264 was requested for Pi Decoder. Falling back to it."); + chiaki_connect_info.video_profile.codec = CHIAKI_CODEC_H264; + } +#endif + if(connect_info.regist_key.size() != sizeof(chiaki_connect_info.regist_key)) throw ChiakiException("RegistKey invalid"); memcpy(chiaki_connect_info.regist_key, connect_info.regist_key.constData(), sizeof(chiaki_connect_info.regist_key)); From a2955d21fc565fc85e5570e6ff9d742bbd387204 Mon Sep 17 00:00:00 2001 From: Alexander Millin Date: Fri, 1 Jan 2021 21:23:19 +0100 Subject: [PATCH 143/237] Enable hevc_vaapi in FFMPEG Builds --- scripts/build-ffmpeg.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/build-ffmpeg.sh b/scripts/build-ffmpeg.sh index 4761520..715a87f 100755 --- a/scripts/build-ffmpeg.sh +++ b/scripts/build-ffmpeg.sh @@ -9,6 +9,6 @@ TAG=n4.3.1 git clone https://git.ffmpeg.org/ffmpeg.git --depth 1 -b $TAG && cd ffmpeg || exit 1 -./configure --disable-all --enable-avcodec --enable-decoder=h264 --enable-decoder=hevc --enable-hwaccel=h264_vaapi --prefix="$ROOT/ffmpeg-prefix" "$@" || exit 1 +./configure --disable-all --enable-avcodec --enable-decoder=h264 --enable-decoder=hevc --enable-hwaccel=h264_vaapi --enable-hwaccel=hevc_vaapi --prefix="$ROOT/ffmpeg-prefix" "$@" || exit 1 make -j4 || exit 1 make install || exit 1 From d4dc0ffee19ccc29764dff2f8e298a632eab2c7c Mon Sep 17 00:00:00 2001 From: h0neybadger Date: Sat, 2 Jan 2021 13:23:14 +0100 Subject: [PATCH 144/237] Fix switch README info and nxlink push script --- scripts/switch/push-docker-build-chiaki.sh | 10 ++++++---- switch/README.md | 20 ++------------------ 2 files changed, 8 insertions(+), 22 deletions(-) diff --git a/scripts/switch/push-docker-build-chiaki.sh b/scripts/switch/push-docker-build-chiaki.sh index ff66e40..ac82029 100755 --- a/scripts/switch/push-docker-build-chiaki.sh +++ b/scripts/switch/push-docker-build-chiaki.sh @@ -3,8 +3,10 @@ cd "`dirname $(readlink -f ${0})`/../.." docker run \ - -v "`pwd`:/build/chiaki" \ - -p 28771:28771 -ti \ - chiaki-switch \ - -c "/opt/devkitpro/tools/bin/nxlink -a $@ -s /build/chiaki/build_switch/switch/chiaki.nro" + -v "`pwd`:/build/chiaki" \ + -w "/build/chiaki" \ + -ti -p 28771:28771 \ + --entrypoint /opt/devkitpro/tools/bin/nxlink \ + thestr4ng3r/chiaki-build-switch \ + "$@" -s /build/chiaki/build_switch/switch/chiaki.nro diff --git a/switch/README.md b/switch/README.md index 709f900..f5fbe68 100644 --- a/switch/README.md +++ b/switch/README.md @@ -4,22 +4,6 @@ this project requires the devkitpro toolchain. you can use your personal computer to install devkitpro but the easiest way is to use the following container. -Build container image ---------------------- -``` -bash scripts/switch/build-docker-image.sh -``` - -Run container -------------- -from the project's [root folder](../) -``` -docker run -it --rm \ - -v "$(pwd):/build" \ - -p 28771:28771 \ - chiaki-switch -``` - Build Project ------------- ``` @@ -31,7 +15,7 @@ tools Push to homebrew Netloader ``` # where X.X.X.X is the IP of your switch -scripts/switch/push-docker-build-chiaki.sh 192.168.0.200 +bash scripts/switch/push-docker-build-chiaki.sh -a 192.168.0.200 ``` Troubleshoot @@ -52,7 +36,7 @@ this file contains sensitive data. (do not share this file) [PS*-***] # required: lan PlayStation IP address # IP from Settings > System > system information -host_ip = *.*.*.* +host_addr = *.*.*.* # required: sony oline id (login) psn_online_id = ps_online_id # required (PS4>7.0 Only): https://git.sr.ht/~thestr4ng3r/chiaki/tree/master/item/README.md#obtaining-your-psn-accountid From 7cf370c70dc4680af06acb3008f8625b0ff1fe75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Mon, 4 Jan 2021 11:16:28 +0100 Subject: [PATCH 145/237] Show SDL GUID in Controller Name in GUI --- gui/src/controllermanager.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/gui/src/controllermanager.cpp b/gui/src/controllermanager.cpp index a78fd44..667b736 100644 --- a/gui/src/controllermanager.cpp +++ b/gui/src/controllermanager.cpp @@ -251,7 +251,11 @@ QString Controller::GetName() #ifdef CHIAKI_GUI_ENABLE_SDL_GAMECONTROLLER if(!controller) return QString(); - return SDL_GameControllerName(controller); + SDL_Joystick *js = SDL_GameControllerGetJoystick(controller); + SDL_JoystickGUID guid = SDL_JoystickGetGUID(js); + char guid_str[256]; + SDL_JoystickGetGUIDString(guid, guid_str, sizeof(guid_str)); + return QString("%1 (%2)").arg(SDL_JoystickName(js), guid_str); #else return QString(); #endif From e531a90d2670590b493244b7bf1d07088e4b194d Mon Sep 17 00:00:00 2001 From: h0neybadger Date: Sat, 2 Jan 2021 13:45:56 +0100 Subject: [PATCH 146/237] Fix switch settings psn id regex --- switch/include/settings.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/switch/include/settings.h b/switch/include/settings.h index 08ecbb2..fbf43b8 100644 --- a/switch/include/settings.h +++ b/switch/include/settings.h @@ -50,7 +50,7 @@ class Settings const std::map re_map = { {HOST_NAME, std::regex("^\\[\\s*(.+)\\s*\\]")}, {HOST_ADDR, std::regex("^\\s*host_(?:ip|addr)\\s*=\\s*\"?((\\d+\\.\\d+\\.\\d+\\.\\d+)|([A-Za-z0-9-]{1,255}))\"?")}, - {PSN_ONLINE_ID, std::regex("^\\s*psn_online_id\\s*=\\s*\"?(\\w+)\"?")}, + {PSN_ONLINE_ID, std::regex("^\\s*psn_online_id\\s*=\\s*\"?([\\w_-]+)\"?")}, {PSN_ACCOUNT_ID, std::regex("^\\s*psn_account_id\\s*=\\s*\"?([\\w/=+]+)\"?")}, {RP_KEY, std::regex("^\\s*rp_key\\s*=\\s*\"?([\\w/=+]+)\"?")}, {RP_KEY_TYPE, std::regex("^\\s*rp_key_type\\s*=\\s*\"?(\\d)\"?")}, From b9a9ea497c970f6dd6a0b210adca96f3f288091e Mon Sep 17 00:00:00 2001 From: h0neybadger Date: Mon, 4 Jan 2021 11:08:57 +0100 Subject: [PATCH 147/237] Fix switch audio delay with SDL_ClearQueuedAudio --- switch/src/io.cpp | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/switch/src/io.cpp b/switch/src/io.cpp index 9f6ab56..3104bd4 100644 --- a/switch/src/io.cpp +++ b/switch/src/io.cpp @@ -291,6 +291,16 @@ void IO::AudioCB(int16_t *buf, size_t samples_count) else buf[x] = (int16_t)sample; } + + int audio_queued_size = SDL_GetQueuedAudioSize(this->sdl_audio_device_id); + if(audio_queued_size > 16000) + { + // clear audio queue to avoid big audio delay + // average values are close to 13000 bytes + CHIAKI_LOGW(this->log, "Triggering SDL_ClearQueuedAudio with queue size = %d", audio_queued_size); + SDL_ClearQueuedAudio(this->sdl_audio_device_id); + } + int success = SDL_QueueAudio(this->sdl_audio_device_id, buf, sizeof(int16_t) * samples_count * 2); if(success != 0) CHIAKI_LOGE(this->log, "SDL_QueueAudio failed: %s\n", SDL_GetError()); From 1ee23e0fa2c5f514acc9e2ae19b15a055e47628e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Mon, 4 Jan 2021 12:44:13 +0100 Subject: [PATCH 148/237] Don't mess with the OMX Buffer Size --- lib/src/pidecoder.c | 105 ++++++++++++++++++-------------------------- 1 file changed, 43 insertions(+), 62 deletions(-) diff --git a/lib/src/pidecoder.c b/lib/src/pidecoder.c index 72a22f5..ae99fbd 100644 --- a/lib/src/pidecoder.c +++ b/lib/src/pidecoder.c @@ -8,7 +8,6 @@ #include #include -#define MAX_DECODE_UNIT_SIZE 262144 CHIAKI_EXPORT ChiakiErrorCode chiaki_pi_decoder_init(ChiakiPiDecoder *decoder, ChiakiLog *log) { @@ -108,29 +107,6 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_pi_decoder_init(ChiakiPiDecoder *decoder, C return CHIAKI_ERR_UNKNOWN; } - OMX_PARAM_PORTDEFINITIONTYPE port; - - memset(&port, 0, sizeof(OMX_PARAM_PORTDEFINITIONTYPE)); - port.nSize = sizeof(OMX_PARAM_PORTDEFINITIONTYPE); - port.nVersion.nVersion = OMX_VERSION; - port.nPortIndex = 130; - if(OMX_GetParameter(ILC_GET_HANDLE(decoder->video_decode), OMX_IndexParamPortDefinition, &port) != OMX_ErrorNone) - { - CHIAKI_LOGE(decoder->log, "Failed to get decoder port definition\n"); - chiaki_pi_decoder_fini(decoder); - return CHIAKI_ERR_UNKNOWN; - } - - // Increase the buffer size to fit the largest possible frame - port.nBufferSize = MAX_DECODE_UNIT_SIZE; - - if(OMX_SetParameter(ILC_GET_HANDLE(decoder->video_decode), OMX_IndexParamPortDefinition, &port) != OMX_ErrorNone) - { - CHIAKI_LOGE(decoder->log, "OMX_SetParameter failed for port"); - chiaki_pi_decoder_fini(decoder); - return CHIAKI_ERR_UNKNOWN; - } - if(ilclient_enable_port_buffers(decoder->video_decode, 130, NULL, NULL, NULL) != 0) { CHIAKI_LOGE(decoder->log, "ilclient_enable_port_buffers failed"); @@ -180,50 +156,55 @@ CHIAKI_EXPORT void chiaki_pi_decoder_fini(ChiakiPiDecoder *decoder) static bool push_buffer(ChiakiPiDecoder *decoder, uint8_t *buf, size_t buf_size) { - OMX_BUFFERHEADERTYPE *omx_buf = ilclient_get_input_buffer(decoder->video_decode, 130, 1); - if(!omx_buf) + while(buf_size) { - CHIAKI_LOGE(decoder->log, "ilclient_get_input_buffer failed"); - return false; - } - - if(omx_buf->nAllocLen < buf_size) - { - CHIAKI_LOGE(decoder->log, "Buffer from omx is too small for frame"); - return false; - } - - omx_buf->nFilledLen = 0; - omx_buf->nOffset = 0; - omx_buf->nFlags = OMX_BUFFERFLAG_ENDOFFRAME; - if(decoder->first_packet) - { - omx_buf->nFlags |= OMX_BUFFERFLAG_STARTTIME; - decoder->first_packet = false; - } - - memcpy(omx_buf->pBuffer + omx_buf->nFilledLen, buf, buf_size); - omx_buf->nFilledLen += buf_size; - - if(!decoder->port_settings_changed - && ((omx_buf->nFilledLen > 0 && ilclient_remove_event(decoder->video_decode, OMX_EventPortSettingsChanged, 131, 0, 0, 1) == 0) - || (omx_buf->nFilledLen == 0 && ilclient_wait_for_event(decoder->video_decode, OMX_EventPortSettingsChanged, 131, 0, 0, 1, ILCLIENT_EVENT_ERROR | ILCLIENT_PARAMETER_CHANGED, 10000) == 0))) - { - decoder->port_settings_changed = true; - - if(ilclient_setup_tunnel(decoder->tunnel, 0, 0) != 0) + OMX_BUFFERHEADERTYPE *omx_buf = ilclient_get_input_buffer(decoder->video_decode, 130, 1); + if(!omx_buf) { - CHIAKI_LOGE(decoder->log, "ilclient_setup_tunnel failed"); + CHIAKI_LOGE(decoder->log, "ilclient_get_input_buffer failed"); return false; } - ilclient_change_component_state(decoder->video_render, OMX_StateExecuting); - } + size_t push_size = buf_size; + if(push_size > omx_buf->nAllocLen) + { + CHIAKI_LOGW(decoder->log, "OMX Buffer too small, fragmenting to multiple buffers."); + push_size = omx_buf->nAllocLen; + } + memcpy(omx_buf->pBuffer, buf, push_size); + buf_size -= push_size; + buf += push_size; + omx_buf->nFilledLen = push_size; + omx_buf->nOffset = 0; + omx_buf->nFlags = 0; + if(!buf_size) + omx_buf->nFlags |= OMX_BUFFERFLAG_ENDOFFRAME; + if(decoder->first_packet) + { + omx_buf->nFlags |= OMX_BUFFERFLAG_STARTTIME; + decoder->first_packet = false; + } - if(OMX_EmptyThisBuffer(ILC_GET_HANDLE(decoder->video_decode), omx_buf) != OMX_ErrorNone) - { - CHIAKI_LOGE(decoder->log, "OMX_EmptyThisBuffer failed"); - return false; + if(!decoder->port_settings_changed + && ((omx_buf->nFilledLen > 0 && ilclient_remove_event(decoder->video_decode, OMX_EventPortSettingsChanged, 131, 0, 0, 1) == 0) + || (omx_buf->nFilledLen == 0 && ilclient_wait_for_event(decoder->video_decode, OMX_EventPortSettingsChanged, 131, 0, 0, 1, ILCLIENT_EVENT_ERROR | ILCLIENT_PARAMETER_CHANGED, 10000) == 0))) + { + decoder->port_settings_changed = true; + + if(ilclient_setup_tunnel(decoder->tunnel, 0, 0) != 0) + { + CHIAKI_LOGE(decoder->log, "ilclient_setup_tunnel failed"); + return false; + } + + ilclient_change_component_state(decoder->video_render, OMX_StateExecuting); + } + + if(OMX_EmptyThisBuffer(ILC_GET_HANDLE(decoder->video_decode), omx_buf) != OMX_ErrorNone) + { + CHIAKI_LOGE(decoder->log, "OMX_EmptyThisBuffer failed"); + return false; + } } return true; } From 0af3ae27d45345de7bdf8dc0a2b83a8ac2b808cf Mon Sep 17 00:00:00 2001 From: h0neybadger Date: Mon, 4 Jan 2021 17:22:50 +0100 Subject: [PATCH 149/237] Use borealis keyboard for the nintendo switch --- switch/borealis | 2 +- switch/include/gui.h | 4 - switch/include/host.h | 2 +- switch/include/io.h | 1 - switch/src/gui.cpp | 203 +++++++++++++++++++----------------------- switch/src/host.cpp | 13 ++- switch/src/io.cpp | 36 -------- 7 files changed, 98 insertions(+), 163 deletions(-) diff --git a/switch/borealis b/switch/borealis index 205e97a..cbdc1b6 160000 --- a/switch/borealis +++ b/switch/borealis @@ -1 +1 @@ -Subproject commit 205e97ab45922fa7f5c9fa6a85d5d686cd50b669 +Subproject commit cbdc1b65314d1eeb2799deae5cf6f113d6d67b46 diff --git a/switch/include/gui.h b/switch/include/gui.h index afd2bab..38f13ec 100644 --- a/switch/include/gui.h +++ b/switch/include/gui.h @@ -50,10 +50,6 @@ class MainApplication IO *io; brls::TabFrame *rootFrame; std::map host_menuitems; - // add_host local settings - std::string remote_display_name = ""; - std::string remote_addr = ""; - ChiakiTarget remote_ps_version = CHIAKI_TARGET_PS5_1; bool BuildConfigurationMenu(brls::List *, Host *host = nullptr); void BuildAddHostConfigurationMenu(brls::List *); diff --git a/switch/include/host.h b/switch/include/host.h index ae6f05e..a93e7c9 100644 --- a/switch/include/host.h +++ b/switch/include/host.h @@ -90,7 +90,7 @@ class Host public: Host(std::string host_name); ~Host(); - int Register(std::string pin); + int Register(int pin); int Wakeup(); int InitSession(IO *); int FiniSession(); diff --git a/switch/include/io.h b/switch/include/io.h index 8427684..85e2966 100644 --- a/switch/include/io.h +++ b/switch/include/io.h @@ -102,7 +102,6 @@ class IO bool FreeVideo(); bool InitJoystick(); bool FreeJoystick(); - bool ReadUserKeyboard(char *buffer, size_t buffer_size); bool MainLoop(ChiakiControllerState *state); }; diff --git a/switch/src/gui.cpp b/switch/src/gui.cpp index d784026..f31cba8 100644 --- a/switch/src/gui.cpp +++ b/switch/src/gui.cpp @@ -97,42 +97,31 @@ void HostInterface::Register(Host *host, std::function success_cb) host->SetRegistEventTypeFinishedFailed(event_type_finished_failed_cb); // the host is not registered yet - brls::Dialog *peprpc = new brls::Dialog("Please enter your PlayStation registration PIN code"); - brls::GenericEvent::Callback cb_peprpc = [host, io, peprpc](brls::View *view) { - bool pin_provided = false; - char pin_input[9] = { 0 }; - std::string error_message; - - // use callback to ensure that the message is showed on screen - // before the the ReadUserKeyboard - peprpc->close(); - - pin_provided = io->ReadUserKeyboard(pin_input, sizeof(pin_input)); - if(pin_provided) + // use callback to ensure that the message is showed on screen + // before the Swkbd + auto pin_input_cb = [host](int pin) { + // prevent users form messing with the gui + brls::Application::blockInputs(); + int ret = host->Register(pin); + if(ret != HOST_REGISTER_OK) { - // prevent users form messing with the gui - brls::Application::blockInputs(); - int ret = host->Register(pin_input); - if(ret != HOST_REGISTER_OK) + switch(ret) { - switch(ret) - { - // account not configured - case HOST_REGISTER_ERROR_SETTING_PSNACCOUNTID: - brls::Application::notify("No PSN Account ID provided"); - brls::Application::unblockInputs(); - break; - case HOST_REGISTER_ERROR_SETTING_PSNONLINEID: - brls::Application::notify("No PSN Online ID provided"); - brls::Application::unblockInputs(); - break; - } + // account not configured + case HOST_REGISTER_ERROR_SETTING_PSNACCOUNTID: + brls::Application::notify("No PSN Account ID provided"); + brls::Application::unblockInputs(); + break; + case HOST_REGISTER_ERROR_SETTING_PSNONLINEID: + brls::Application::notify("No PSN Online ID provided"); + brls::Application::unblockInputs(); + break; } } }; - peprpc->addButton("Ok", cb_peprpc); - peprpc->setCancelable(false); - peprpc->open(); + // the pin is 8 digit + bool success = brls::Swkbd::openForNumber(pin_input_cb, + "Please enter your PlayStation registration PIN code", "8 digits without spaces", 8, "", "", ""); } void HostInterface::Register() @@ -327,39 +316,36 @@ bool MainApplication::Load() bool MainApplication::BuildConfigurationMenu(brls::List *ls, Host *host) { std::string psn_account_id_string = this->settings->GetPSNAccountID(host); - brls::ListItem *psn_account_id = new brls::ListItem("PSN Account ID", "PS5 or PS4 v7.0 and greater (base64 account_id)"); - psn_account_id->setValue(psn_account_id_string.c_str()); + brls::InputListItem *psn_account_id = new brls::InputListItem("PSN Account ID", psn_account_id_string, + "Account ID in base64 format", "PS5 or PS4 v7.0 and greater", CHIAKI_PSN_ACCOUNT_ID_SIZE * 2, + brls::KeyboardKeyDisableBitmask::KEYBOARD_DISABLE_SPACE | + brls::KeyboardKeyDisableBitmask::KEYBOARD_DISABLE_AT | + brls::KeyboardKeyDisableBitmask::KEYBOARD_DISABLE_PERCENT | + brls::KeyboardKeyDisableBitmask::KEYBOARD_DISABLE_BACKSLASH); + auto psn_account_id_cb = [this, host, psn_account_id](brls::View *view) { - char account_id[CHIAKI_PSN_ACCOUNT_ID_SIZE * 2] = { 0 }; - bool input = this->io->ReadUserKeyboard(account_id, sizeof(account_id)); - if(input) - { - // update gui - psn_account_id->setValue(account_id); - // push in setting - this->settings->SetPSNAccountID(host, account_id); - // write on disk - this->settings->WriteFile(); - } + // retrieve, push and save setting + this->settings->SetPSNAccountID(host, psn_account_id->getValue()); + // write on disk + this->settings->WriteFile(); }; psn_account_id->getClickEvent()->subscribe(psn_account_id_cb); ls->addView(psn_account_id); std::string psn_online_id_string = this->settings->GetPSNOnlineID(host); - brls::ListItem *psn_online_id = new brls::ListItem("PSN Online ID"); - psn_online_id->setValue(psn_online_id_string.c_str()); + brls::InputListItem *psn_online_id = new brls::InputListItem("PSN Online ID", + psn_online_id_string, "", "", 16, + brls::KeyboardKeyDisableBitmask::KEYBOARD_DISABLE_SPACE | + brls::KeyboardKeyDisableBitmask::KEYBOARD_DISABLE_AT | + brls::KeyboardKeyDisableBitmask::KEYBOARD_DISABLE_PERCENT | + brls::KeyboardKeyDisableBitmask::KEYBOARD_DISABLE_FORWSLASH | + brls::KeyboardKeyDisableBitmask::KEYBOARD_DISABLE_BACKSLASH); + auto psn_online_id_cb = [this, host, psn_online_id](brls::View *view) { - char online_id[256] = { 0 }; - bool input = this->io->ReadUserKeyboard(online_id, sizeof(online_id)); - if(input) - { - // update gui - psn_online_id->setValue(online_id); - // push in setting - this->settings->SetPSNOnlineID(host, online_id); - // write on disk - this->settings->WriteFile(); - } + // retrieve, push and save setting + this->settings->SetPSNOnlineID(host, psn_online_id->getValue()); + // write on disk + this->settings->WriteFile(); }; psn_online_id->getClickEvent()->subscribe(psn_online_id_cb); ls->addView(psn_online_id); @@ -465,34 +451,24 @@ void MainApplication::BuildAddHostConfigurationMenu(brls::List *add_host) // brls::Label* add_host_label = new brls::Label(brls::LabelStyle::REGULAR, // "Add Host configuration", true); - brls::ListItem *display_name = new brls::ListItem("Display name"); - auto display_name_cb = [this, display_name](brls::View *view) { - char name[16] = { 0 }; - bool input = this->io->ReadUserKeyboard(name, sizeof(name)); - if(input) - { - // update gui - display_name->setValue(name); - // set internal value - this->remote_display_name = name; - } - }; - display_name->getClickEvent()->subscribe(display_name_cb); + brls::InputListItem *display_name = new brls::InputListItem("Display name", + "default", "configuration name", "", 16, + brls::KeyboardKeyDisableBitmask::KEYBOARD_DISABLE_SPACE | + brls::KeyboardKeyDisableBitmask::KEYBOARD_DISABLE_AT | + brls::KeyboardKeyDisableBitmask::KEYBOARD_DISABLE_PERCENT | + brls::KeyboardKeyDisableBitmask::KEYBOARD_DISABLE_FORWSLASH | + brls::KeyboardKeyDisableBitmask::KEYBOARD_DISABLE_BACKSLASH); + add_host->addView(display_name); - brls::ListItem *address = new brls::ListItem("Remote IP/name"); - auto address_cb = [this, address](brls::View *view) { - char addr[256] = { 0 }; - bool input = this->io->ReadUserKeyboard(addr, sizeof(addr)); - if(input) - { - // update gui - address->setValue(addr); - // set internal value - this->remote_addr = addr; - } - }; - address->getClickEvent()->subscribe(address_cb); + brls::InputListItem *address = new brls::InputListItem("Remote IP/name", + "", "IP address or fqdn", "", 255, + brls::KeyboardKeyDisableBitmask::KEYBOARD_DISABLE_SPACE | + brls::KeyboardKeyDisableBitmask::KEYBOARD_DISABLE_AT | + brls::KeyboardKeyDisableBitmask::KEYBOARD_DISABLE_PERCENT | + brls::KeyboardKeyDisableBitmask::KEYBOARD_DISABLE_FORWSLASH | + brls::KeyboardKeyDisableBitmask::KEYBOARD_DISABLE_BACKSLASH); + add_host->addView(address); // TODO @@ -501,46 +477,48 @@ void MainApplication::BuildAddHostConfigurationMenu(brls::List *add_host) // brls::ListItem* port = new brls::ListItem("Remote Senkusha port", "udp 9297"); brls::SelectListItem *ps_version = new brls::SelectListItem("PlayStation Version", { "PS5", "PS4 > 8", "7 < PS4 < 8", "PS4 < 7" }); - auto ps_version_cb = [this, ps_version](int result) { - switch(result) - { - case 0: - // ps5 v1 - this->remote_ps_version = CHIAKI_TARGET_PS5_1; - break; - case 1: - // ps4 v8 - this->remote_ps_version = CHIAKI_TARGET_PS4_10; - break; - case 2: - // ps4 v7 - this->remote_ps_version = CHIAKI_TARGET_PS4_9; - break; - case 3: - // ps4 v6 - this->remote_ps_version = CHIAKI_TARGET_PS4_8; - break; - } - }; - ps_version->getValueSelectedEvent()->subscribe(ps_version_cb); add_host->addView(ps_version); brls::ListItem *register_host = new brls::ListItem("Register"); - auto register_host_cb = [this](brls::View *view) { + auto register_host_cb = [this, display_name, address, ps_version](brls::View *view) { bool err = false; - if(this->remote_display_name.length() <= 0) + std::string dn = display_name->getValue(); + std::string addr = address->getValue(); + ChiakiTarget version = CHIAKI_TARGET_PS4_UNKNOWN; + + switch(ps_version->getSelectedValue()) + { + case 0: + // ps5 v1 + version = CHIAKI_TARGET_PS5_1; + break; + case 1: + // ps4 v8 + version = CHIAKI_TARGET_PS4_10; + break; + case 2: + // ps4 v7 + version = CHIAKI_TARGET_PS4_9; + break; + case 3: + // ps4 v6 + version = CHIAKI_TARGET_PS4_8; + break; + } + + if(dn.length() <= 0) { brls::Application::notify("No Display name defined"); err = true; } - if(this->remote_addr.length() <= 0) + if(addr.length() <= 0) { brls::Application::notify("No Remote address provided"); err = true; } - if(this->remote_ps_version < 0) + if(version <= CHIAKI_TARGET_PS4_UNKNOWN) { brls::Application::notify("No PlayStation Version provided"); err = true; @@ -549,10 +527,9 @@ void MainApplication::BuildAddHostConfigurationMenu(brls::List *add_host) if(err) return; - Host *host = this->settings->GetOrCreateHost(&this->remote_display_name); - host->SetHostAddr(this->remote_addr); - host->SetChiakiTarget(this->remote_ps_version); - + Host *host = this->settings->GetOrCreateHost(&dn); + host->SetHostAddr(addr); + host->SetChiakiTarget(version); HostInterface::Register(host); }; register_host->getClickEvent()->subscribe(register_host_cb); diff --git a/switch/src/host.cpp b/switch/src/host.cpp index 6db9c50..50fdd2f 100644 --- a/switch/src/host.cpp +++ b/switch/src/host.cpp @@ -72,7 +72,7 @@ int Host::Wakeup() return ret; } -int Host::Register(std::string pin) +int Host::Register(int pin) { // use pin and accont_id to negociate secrets for session // @@ -103,7 +103,7 @@ int Host::Register(std::string pin) if(online_id.length() > 0) { regist_info.psn_online_id = this->psn_online_id.c_str(); - // regist_info.psn_account_id = {0}; + // regist_info.psn_account_id = '\0'; } else { @@ -117,16 +117,15 @@ int Host::Register(std::string pin) throw Exception("Undefined PS4 system version (please run discover first)"); } - this->regist_info.pin = atoi(pin.c_str()); this->regist_info.host = this->host_addr.c_str(); this->regist_info.broadcast = false; if(this->target >= CHIAKI_TARGET_PS4_9) - CHIAKI_LOGI(this->log, "Registering to host `%s` `%s` with PSN AccountID `%s` pin `%s`", - this->host_name.c_str(), this->host_addr.c_str(), account_id.c_str(), pin.c_str()); + CHIAKI_LOGI(this->log, "Registering to host `%s` `%s` with PSN AccountID `%s` pin `%d`", + this->host_name.c_str(), this->host_addr.c_str(), account_id.c_str(), pin); else - CHIAKI_LOGI(this->log, "Registering to host `%s` `%s` with PSN OnlineID `%s` pin `%s`", - this->host_name.c_str(), this->host_addr.c_str(), online_id.c_str(), pin.c_str()); + CHIAKI_LOGI(this->log, "Registering to host `%s` `%s` with PSN OnlineID `%s` pin `%d`", + this->host_name.c_str(), this->host_addr.c_str(), online_id.c_str(), pin); chiaki_regist_start(&this->regist, this->log, &this->regist_info, RegistEventCB, this); return HOST_REGISTER_OK; diff --git a/switch/src/io.cpp b/switch/src/io.cpp index 3104bd4..e086215 100644 --- a/switch/src/io.cpp +++ b/switch/src/io.cpp @@ -345,42 +345,6 @@ bool IO::FreeVideo() return ret; } -bool IO::ReadUserKeyboard(char *buffer, size_t buffer_size) -{ -#ifndef __SWITCH__ - // use cin to get user input from linux - std::cin.getline(buffer, buffer_size); - CHIAKI_LOGI(this->log, "Got user input: %s\n", buffer); -#else - // https://kvadevack.se/post/nintendo-switch-virtual-keyboard/ - SwkbdConfig kbd; - Result rc = swkbdCreate(&kbd, 0); - - if(R_SUCCEEDED(rc)) - { - swkbdConfigMakePresetDefault(&kbd); - rc = swkbdShow(&kbd, buffer, buffer_size); - - if(R_SUCCEEDED(rc)) - { - CHIAKI_LOGI(this->log, "Got user input: %s\n", buffer); - } - else - { - CHIAKI_LOGE(this->log, "swkbdShow() error: %u\n", rc); - return false; - } - swkbdClose(&kbd); - } - else - { - CHIAKI_LOGE(this->log, "swkbdCreate() error: %u\n", rc); - return false; - } -#endif - return true; -} - bool IO::ReadGameTouchScreen(ChiakiControllerState *state) { #ifdef __SWITCH__ From c5246541a92cff966105cd08ec1218e3da287e7c Mon Sep 17 00:00:00 2001 From: h0neybadger Date: Mon, 4 Jan 2021 23:04:04 +0100 Subject: [PATCH 150/237] Fix switch target chiaki.conf format --- switch/src/settings.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/switch/src/settings.cpp b/switch/src/settings.cpp index f5edf1f..8cb5264 100644 --- a/switch/src/settings.cpp +++ b/switch/src/settings.cpp @@ -210,9 +210,8 @@ int Settings::WriteFile() config_file << "[" << it->first << "]\n" << "host_addr = \"" << it->second.GetHostAddr() << "\"\n" - << "target = " << it->second.GetChiakiTarget() << "\"\n"; + << "target = \"" << it->second.GetChiakiTarget() << "\"\n"; - config_file << "target = \"" << it->second.psn_account_id << "\"\n"; if(it->second.video_resolution) config_file << "video_resolution = \"" << this->ResolutionPresetToString(this->GetVideoResolution(&it->second)) From 3272f47dc67259292758ed69998e4c0b5c4ff717 Mon Sep 17 00:00:00 2001 From: Roy P Date: Wed, 6 Jan 2021 15:56:32 +0100 Subject: [PATCH 151/237] Update Flatpak to v2.0.1 --- scripts/flatpak/com.github.thestr4ng3r.Chiaki.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/flatpak/com.github.thestr4ng3r.Chiaki.json b/scripts/flatpak/com.github.thestr4ng3r.Chiaki.json index 7cc6c43..3a03107 100644 --- a/scripts/flatpak/com.github.thestr4ng3r.Chiaki.json +++ b/scripts/flatpak/com.github.thestr4ng3r.Chiaki.json @@ -89,8 +89,8 @@ { "type": "git", "url": "https://git.sr.ht/~thestr4ng3r/chiaki", - "tag": "v1.3.0", - "commit": "702d31eb01d37518e77f5c1be3ea493df9f18323" + "tag": "v2.0.1", + "commit": "9e698dd7c4e4011ff6e136741abef5cf4b32527c" } ] } From 0e324a41a0c2738cabfa487e111abc55c24185f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Tue, 5 Jan 2021 14:41:35 +0100 Subject: [PATCH 152/237] Fix setting a Feedback State Byte --- lib/src/feedback.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/feedback.c b/lib/src/feedback.c index d5a46bc..4c52b72 100644 --- a/lib/src/feedback.c +++ b/lib/src/feedback.c @@ -38,7 +38,7 @@ CHIAKI_EXPORT void chiaki_feedback_state_format_v9(uint8_t *buf, ChiakiFeedbackS CHIAKI_EXPORT void chiaki_feedback_state_format_v12(uint8_t *buf, ChiakiFeedbackState *state) { chiaki_feedback_state_format_v9(buf, state); - buf[0x10] = 0x0; + buf[0x19] = 0x0; buf[0x1a] = 0x0; buf[0x1b] = 0x1; // 1 for Shock, 0 for Sense } From abc9a27208592e34fce2fe850a35222f9f2efd1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Wed, 6 Jan 2021 11:31:13 +0100 Subject: [PATCH 153/237] Add Motion Stub to Setsu --- gui/src/streamsession.cpp | 2 +- setsu/CMakeLists.txt | 9 +- setsu/demo/{main.c => touchpad.c} | 6 +- setsu/include/setsu.h | 21 ++- setsu/src/setsu.c | 288 ++++++++++++++++++------------ 5 files changed, 196 insertions(+), 130 deletions(-) rename setsu/demo/{main.c => touchpad.c} (94%) diff --git a/gui/src/streamsession.cpp b/gui/src/streamsession.cpp index 01008a8..52915f0 100644 --- a/gui/src/streamsession.cpp +++ b/gui/src/streamsession.cpp @@ -409,7 +409,7 @@ void StreamSession::HandleSetsuEvent(SetsuEvent *event) switch(event->type) { case SETSU_EVENT_DEVICE_ADDED: - setsu_connect(setsu, event->path); + setsu_connect(setsu, event->path, event->dev_type); break; case SETSU_EVENT_DEVICE_REMOVED: for(auto it=setsu_ids.begin(); it!=setsu_ids.end();) diff --git a/setsu/CMakeLists.txt b/setsu/CMakeLists.txt index 90c1465..186f9c0 100644 --- a/setsu/CMakeLists.txt +++ b/setsu/CMakeLists.txt @@ -3,7 +3,7 @@ cmake_minimum_required(VERSION 3.2) project(libsetsu) -option(SETSU_BUILD_DEMO "Build testing executable for libsetsu" OFF) +option(SETSU_BUILD_DEMOS "Build testing executables for libsetsu" OFF) list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake") @@ -17,9 +17,8 @@ find_package(Udev REQUIRED) find_package(Evdev REQUIRED) target_link_libraries(setsu Udev::libudev Evdev::libevdev) -if(SETSU_BUILD_DEMO) - add_executable(setsu-demo - demo/main.c) - target_link_libraries(setsu-demo setsu) +if(SETSU_BUILD_DEMOS) + add_executable(setsu-demo-touchpad demo/touchpad.c) + target_link_libraries(setsu-demo-touchpad setsu) endif() diff --git a/setsu/demo/main.c b/setsu/demo/touchpad.c similarity index 94% rename from setsu/demo/main.c rename to setsu/demo/touchpad.c index 4219638..5f7deff 100644 --- a/setsu/demo/main.c +++ b/setsu/demo/touchpad.c @@ -68,11 +68,15 @@ void event(SetsuEvent *event, void *user) switch(event->type) { case SETSU_EVENT_DEVICE_ADDED: { - SetsuDevice *dev = setsu_connect(setsu, event->path); + if(event->dev_type != SETSU_DEVICE_TYPE_TOUCHPAD) + break; + SetsuDevice *dev = setsu_connect(setsu, event->path, SETSU_DEVICE_TYPE_TOUCHPAD); LOG("Device added: %s, connect %s\n", event->path, dev ? "succeeded" : "FAILED!"); break; } case SETSU_EVENT_DEVICE_REMOVED: + if(event->dev_type != SETSU_DEVICE_TYPE_TOUCHPAD) + break; LOG("Device removed: %s\n", event->path); break; case SETSU_EVENT_TOUCH_DOWN: diff --git a/setsu/include/setsu.h b/setsu/include/setsu.h index 5e546bc..a2ca4ed 100644 --- a/setsu/include/setsu.h +++ b/setsu/include/setsu.h @@ -13,13 +13,18 @@ typedef struct setsu_t Setsu; typedef struct setsu_device_t SetsuDevice; typedef int SetsuTrackingId; +typedef enum { + SETSU_DEVICE_TYPE_TOUCHPAD, + SETSU_DEVICE_TYPE_MOTION +} SetsuDeviceType; + typedef enum { /* New device available to connect. - * Event will have path set to the new device. */ + * Event will have path and type set to the new device. */ SETSU_EVENT_DEVICE_ADDED, /* Previously available device removed. - * Event will have path set to the new device. + * Event will have path and type set to the removed device. * Any SetsuDevice connected to this path will automatically * be disconnected and their pointers will be invalid immediately * after the callback for this event returns. */ @@ -53,7 +58,11 @@ typedef struct setsu_event_t SetsuEventType type; union { - const char *path; + struct + { + const char *path; + SetsuDeviceType dev_type; + }; struct { SetsuDevice *dev; @@ -75,11 +84,11 @@ typedef void (*SetsuEventCb)(SetsuEvent *event, void *user); Setsu *setsu_new(); void setsu_free(Setsu *setsu); void setsu_poll(Setsu *setsu, SetsuEventCb cb, void *user); -SetsuDevice *setsu_connect(Setsu *setsu, const char *path); +SetsuDevice *setsu_connect(Setsu *setsu, const char *path, SetsuDeviceType type); void setsu_disconnect(Setsu *setsu, SetsuDevice *dev); const char *setsu_device_get_path(SetsuDevice *dev); -uint32_t setsu_device_get_width(SetsuDevice *dev); -uint32_t setsu_device_get_height(SetsuDevice *dev); +uint32_t setsu_device_touchpad_get_width(SetsuDevice *dev); +uint32_t setsu_device_touchpad_get_height(SetsuDevice *dev); #ifdef __cplusplus } diff --git a/setsu/src/setsu.c b/setsu/src/setsu.c index 819b174..b93ef2c 100644 --- a/setsu/src/setsu.c +++ b/setsu/src/setsu.c @@ -25,6 +25,7 @@ typedef struct setsu_avail_device_t { struct setsu_avail_device_t *next; + SetsuDeviceType type; char *path; bool connect_dirty; // whether the connect has not been sent as an event yet bool disconnect_dirty; // whether the disconnect has not been sent as an event yet @@ -36,27 +37,38 @@ typedef struct setsu_device_t { struct setsu_device_t *next; char *path; + SetsuDeviceType type; int fd; struct libevdev *evdev; - int min_x, min_y, max_x, max_y; - struct { - /* Saves the old tracking id that was just up-ed. - * also for handling "atomic" up->down - * i.e. when there is an up, then down with a different tracking id - * in a single frame (before SYN_REPORT), this saves the old - * tracking id that must be reported as up. */ - int tracking_id_prev; + union + { + struct + { + int min_x, min_y, max_x, max_y; - int tracking_id; - int x, y; - bool downed; - bool pos_dirty; - } slots[SLOTS_COUNT]; - unsigned int slot_cur; + struct + { + /* Saves the old tracking id that was just up-ed. + * also for handling "atomic" up->down + * i.e. when there is an up, then down with a different tracking id + * in a single frame (before SYN_REPORT), this saves the old + * tracking id that must be reported as up. */ + int tracking_id_prev; - uint64_t buttons_prev; - uint64_t buttons_cur; + int tracking_id; + int x, y; + bool downed; + bool pos_dirty; + } slots[SLOTS_COUNT]; + unsigned int slot_cur; + uint64_t buttons_prev; + uint64_t buttons_cur; + } touchpad; + struct + { + } motion; + }; } SetsuDevice; struct setsu_t @@ -131,8 +143,8 @@ static void scan_udev(Setsu *setsu) if(udev_enumerate_add_match_subsystem(udev_enum, "input") < 0) goto beach; - if(udev_enumerate_add_match_property(udev_enum, "ID_INPUT_TOUCHPAD", "1") < 0) - goto beach; + //if(udev_enumerate_add_match_property(udev_enum, "ID_INPUT_TOUCHPAD", "1") < 0) + // goto beach; if(udev_enumerate_scan_devices(udev_enum) < 0) goto beach; @@ -153,7 +165,7 @@ beach: udev_enumerate_unref(udev_enum); } -static bool is_device_interesting(struct udev_device *dev) +static bool is_device_interesting(struct udev_device *dev, SetsuDeviceType *type) { static const uint32_t device_ids[] = { // vendor id, model id @@ -162,8 +174,18 @@ static bool is_device_interesting(struct udev_device *dev) 0x54c, 0x0ce6 // DualSense }; - // Filter mouse-device (/dev/input/mouse*) away and only keep the evdev (/dev/input/event*) one: - if(!udev_device_get_property_value(dev, "ID_INPUT_TOUCHPAD_INTEGRATION")) + const char *touchpad_str = udev_device_get_property_value(dev, "ID_INPUT_TOUCHPAD"); + const char *accel_str = udev_device_get_property_value(dev, "ID_INPUT_ACCELEROMETER"); + if(touchpad_str && !strcmp(touchpad_str, "1")) + { + // Filter mouse-device (/dev/input/mouse*) away and only keep the evdev (/dev/input/event*) one: + if(!udev_device_get_property_value(dev, "ID_INPUT_TOUCHPAD_INTEGRATION")) + return false; + *type = SETSU_DEVICE_TYPE_TOUCHPAD; + } + else if(touchpad_str && !strcmp(touchpad_str, "1")) + *type = SETSU_DEVICE_TYPE_MOTION; + else return false; uint32_t vendor; @@ -221,12 +243,14 @@ static void update_udev_device(Setsu *setsu, struct udev_device *dev) } // not yet added - if(!is_device_interesting(dev)) + SetsuDeviceType type; + if(!is_device_interesting(dev, &type)) return; SetsuAvailDevice *adev = calloc(1, sizeof(SetsuAvailDevice)); if(!adev) return; + adev->type = type; adev->path = strdup(path); if(!adev->path) { @@ -272,7 +296,7 @@ bool get_dev_ids(const char *path, uint32_t *vendor_id, uint32_t *model_id) return true; } -SetsuDevice *setsu_connect(Setsu *setsu, const char *path) +SetsuDevice *setsu_connect(Setsu *setsu, const char *path, SetsuDeviceType type) { SetsuDevice *dev = calloc(1, sizeof(SetsuDevice)); if(!dev) @@ -281,6 +305,7 @@ SetsuDevice *setsu_connect(Setsu *setsu, const char *path) dev->path = strdup(path); if(!dev->path) goto error; + dev->type = type; dev->fd = open(dev->path, O_RDONLY | O_NONBLOCK); if(dev->fd == -1) @@ -292,15 +317,23 @@ SetsuDevice *setsu_connect(Setsu *setsu, const char *path) goto error; } - dev->min_x = libevdev_get_abs_minimum(dev->evdev, ABS_X); - dev->min_y = libevdev_get_abs_minimum(dev->evdev, ABS_Y); - dev->max_x = libevdev_get_abs_maximum(dev->evdev, ABS_X); - dev->max_y = libevdev_get_abs_maximum(dev->evdev, ABS_Y); - - for(size_t i=0; islots[i].tracking_id_prev = -1; - dev->slots[i].tracking_id = -1; + case SETSU_DEVICE_TYPE_TOUCHPAD: + dev->touchpad.min_x = libevdev_get_abs_minimum(dev->evdev, ABS_X); + dev->touchpad.min_y = libevdev_get_abs_minimum(dev->evdev, ABS_Y); + dev->touchpad.max_x = libevdev_get_abs_maximum(dev->evdev, ABS_X); + dev->touchpad.max_y = libevdev_get_abs_maximum(dev->evdev, ABS_Y); + + for(size_t i=0; itouchpad.slots[i].tracking_id_prev = -1; + dev->touchpad.slots[i].tracking_id = -1; + } + break; + case SETSU_DEVICE_TYPE_MOTION: + // TODO: init to defaults + break; } dev->next = setsu->dev; @@ -342,14 +375,18 @@ const char *setsu_device_get_path(SetsuDevice *dev) return dev->path; } -uint32_t setsu_device_get_width(SetsuDevice *dev) +uint32_t setsu_device_touchpad_get_width(SetsuDevice *dev) { - return dev->max_x - dev->min_x; + if(dev->type != SETSU_DEVICE_TYPE_TOUCHPAD) + return 0; + return dev->touchpad.max_x - dev->touchpad.min_x; } -uint32_t setsu_device_get_height(SetsuDevice *dev) +uint32_t setsu_device_touchpad_get_height(SetsuDevice *dev) { - return dev->max_y - dev->min_y; + if(dev->type != SETSU_DEVICE_TYPE_TOUCHPAD) + return 0; + return dev->touchpad.max_y - dev->touchpad.min_y; } void kill_avail_device(Setsu *setsu, SetsuAvailDevice *adev) @@ -466,59 +503,68 @@ static void device_event(Setsu *setsu, SetsuDevice *dev, struct input_event *ev, libevdev_event_code_get_name(ev->type, ev->code), ev->value); #endif -#define S dev->slots[dev->slot_cur] - switch(ev->type) + if(ev->type == EV_SYN && ev->code == SYN_REPORT) { - case EV_ABS: - switch(ev->code) + device_drain(setsu, dev, cb, user); + return; + } + switch(dev->type) + { + case SETSU_DEVICE_TYPE_TOUCHPAD: + switch(ev->type) { - case ABS_MT_SLOT: - if((unsigned int)ev->value >= SLOTS_COUNT) + case EV_ABS: +#define S dev->touchpad.slots[dev->touchpad.slot_cur] + switch(ev->code) { - SETSU_LOG("slot too high\n"); + case ABS_MT_SLOT: + if((unsigned int)ev->value >= SLOTS_COUNT) + { + SETSU_LOG("slot too high\n"); + break; + } + dev->touchpad.slot_cur = ev->value; + break; + case ABS_MT_TRACKING_ID: + if(S.tracking_id != -1 && S.tracking_id_prev == -1) + { + // up the tracking id + S.tracking_id_prev = S.tracking_id; + // reset the rest + S.x = S.y = 0; + S.pos_dirty = false; + } + S.tracking_id = ev->value; + if(ev->value != -1) + S.downed = true; + break; + case ABS_MT_POSITION_X: + S.x = ev->value; + S.pos_dirty = true; + break; + case ABS_MT_POSITION_Y: + S.y = ev->value; + S.pos_dirty = true; + break; + } + break; +#undef S + case EV_KEY: { + uint64_t button = button_from_evdev(ev->code); + if(!button) break; - } - dev->slot_cur = ev->value; - break; - case ABS_MT_TRACKING_ID: - if(S.tracking_id != -1 && S.tracking_id_prev == -1) - { - // up the tracking id - S.tracking_id_prev = S.tracking_id; - // reset the rest - S.x = S.y = 0; - S.pos_dirty = false; - } - S.tracking_id = ev->value; - if(ev->value != -1) - S.downed = true; - break; - case ABS_MT_POSITION_X: - S.x = ev->value; - S.pos_dirty = true; - break; - case ABS_MT_POSITION_Y: - S.y = ev->value; - S.pos_dirty = true; + if(ev->value) + dev->touchpad.buttons_cur |= button; + else + dev->touchpad.buttons_cur &= ~button; break; + } } break; - case EV_KEY: { - uint64_t button = button_from_evdev(ev->code); - if(!button) - break; - if(ev->value) - dev->buttons_cur |= button; - else - dev->buttons_cur &= ~button; - break; - } - case EV_SYN: - if(ev->code == SYN_REPORT) - device_drain(setsu, dev, cb, user); + case SETSU_DEVICE_TYPE_MOTION: + // TODO: handle the events break; } -#undef S } static void device_drain(Setsu *setsu, SetsuDevice *dev, SetsuEventCb cb, void *user) @@ -526,48 +572,56 @@ static void device_drain(Setsu *setsu, SetsuDevice *dev, SetsuEventCb cb, void * SetsuEvent event; #define BEGIN_EVENT(tp) do { memset(&event, 0, sizeof(event)); event.dev = dev; event.type = tp; } while(0) #define SEND_EVENT() do { cb(&event, user); } while (0) - for(size_t i=0; itype) { - if(dev->slots[i].tracking_id_prev != -1) - { - BEGIN_EVENT(SETSU_EVENT_TOUCH_UP); - event.tracking_id = dev->slots[i].tracking_id_prev; - SEND_EVENT(); - dev->slots[i].tracking_id_prev = -1; - } - if(dev->slots[i].downed) - { - BEGIN_EVENT(SETSU_EVENT_TOUCH_DOWN); - event.tracking_id = dev->slots[i].tracking_id; - SEND_EVENT(); - dev->slots[i].downed = false; - } - if(dev->slots[i].pos_dirty) - { - BEGIN_EVENT(SETSU_EVENT_TOUCH_POSITION); - event.tracking_id = dev->slots[i].tracking_id; - event.x = (uint32_t)(dev->slots[i].x - dev->min_x); - event.y = (uint32_t)(dev->slots[i].y - dev->min_y); - SEND_EVENT(); - dev->slots[i].pos_dirty = false; - } - } + case SETSU_DEVICE_TYPE_TOUCHPAD: + for(size_t i=0; itouchpad.slots[i].tracking_id_prev != -1) + { + BEGIN_EVENT(SETSU_EVENT_TOUCH_UP); + event.tracking_id = dev->touchpad.slots[i].tracking_id_prev; + SEND_EVENT(); + dev->touchpad.slots[i].tracking_id_prev = -1; + } + if(dev->touchpad.slots[i].downed) + { + BEGIN_EVENT(SETSU_EVENT_TOUCH_DOWN); + event.tracking_id = dev->touchpad.slots[i].tracking_id; + SEND_EVENT(); + dev->touchpad.slots[i].downed = false; + } + if(dev->touchpad.slots[i].pos_dirty) + { + BEGIN_EVENT(SETSU_EVENT_TOUCH_POSITION); + event.tracking_id = dev->touchpad.slots[i].tracking_id; + event.x = (uint32_t)(dev->touchpad.slots[i].x - dev->touchpad.min_x); + event.y = (uint32_t)(dev->touchpad.slots[i].y - dev->touchpad.min_y); + SEND_EVENT(); + dev->touchpad.slots[i].pos_dirty = false; + } + } - uint64_t buttons_diff = dev->buttons_prev ^ dev->buttons_cur; - for(uint64_t i=0; i<64; i++) - { - if(buttons_diff & 1) - { - uint64_t button = 1 << i; - BEGIN_EVENT((dev->buttons_cur & button) ? SETSU_EVENT_BUTTON_DOWN : SETSU_EVENT_BUTTON_UP); - event.button = button; - SEND_EVENT(); - } - buttons_diff >>= 1; - if(!buttons_diff) + uint64_t buttons_diff = dev->touchpad.buttons_prev ^ dev->touchpad.buttons_cur; + for(uint64_t i=0; i<64; i++) + { + if(buttons_diff & 1) + { + uint64_t button = 1 << i; + BEGIN_EVENT((dev->touchpad.buttons_cur & button) ? SETSU_EVENT_BUTTON_DOWN : SETSU_EVENT_BUTTON_UP); + event.button = button; + SEND_EVENT(); + } + buttons_diff >>= 1; + if(!buttons_diff) + break; + } + dev->touchpad.buttons_prev = dev->touchpad.buttons_cur; + break; + case SETSU_DEVICE_TYPE_MOTION: + // TODO break; } - dev->buttons_prev = dev->buttons_cur; #undef BEGIN_EVENT #undef SEND_EVENT } From 20c54b05ad9afc42ce083ba3e6556deb0f137f53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Wed, 6 Jan 2021 11:35:56 +0100 Subject: [PATCH 154/237] Print error on Setsu open failure --- setsu/src/setsu.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/setsu/src/setsu.c b/setsu/src/setsu.c index b93ef2c..520de61 100644 --- a/setsu/src/setsu.c +++ b/setsu/src/setsu.c @@ -309,7 +309,11 @@ SetsuDevice *setsu_connect(Setsu *setsu, const char *path, SetsuDeviceType type) dev->fd = open(dev->path, O_RDONLY | O_NONBLOCK); if(dev->fd == -1) + { + SETSU_LOG("Failed to open %s\n", dev->path); + perror("setsu_connect"); goto error; + } if(libevdev_new_from_fd(dev->fd, &dev->evdev) < 0) { From 698bce80225bfc90c484ba7f790127fab17dc418 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Wed, 6 Jan 2021 13:04:24 +0100 Subject: [PATCH 155/237] Connect Motion Devices in Setsu --- setsu/CMakeLists.txt | 2 + setsu/demo/motion.c | 101 ++++++++++++++++++++++++++++++++++++++++++ setsu/demo/touchpad.c | 2 + setsu/src/setsu.c | 11 ++++- 4 files changed, 114 insertions(+), 2 deletions(-) create mode 100644 setsu/demo/motion.c diff --git a/setsu/CMakeLists.txt b/setsu/CMakeLists.txt index 186f9c0..2dedb0c 100644 --- a/setsu/CMakeLists.txt +++ b/setsu/CMakeLists.txt @@ -20,5 +20,7 @@ target_link_libraries(setsu Udev::libudev Evdev::libevdev) if(SETSU_BUILD_DEMOS) add_executable(setsu-demo-touchpad demo/touchpad.c) target_link_libraries(setsu-demo-touchpad setsu) + add_executable(setsu-demo-motion demo/motion.c) + target_link_libraries(setsu-demo-motion setsu) endif() diff --git a/setsu/demo/motion.c b/setsu/demo/motion.c new file mode 100644 index 0000000..f66e691 --- /dev/null +++ b/setsu/demo/motion.c @@ -0,0 +1,101 @@ +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL + +#include + +#include +#include +#include +#include +#include +#include + +Setsu *setsu; + +bool dirty = false; +bool log_mode; +volatile bool should_quit; + +#define LOG(...) do { if(log_mode) fprintf(stderr, __VA_ARGS__); } while(0) + +void sigint(int s) +{ + should_quit = true; +} + +void print_state() +{ +#if 0 + char buf[256]; + *buf = 0; + printf("\033[2J%s", buf); + fflush(stdout); +#endif +} + +void event(SetsuEvent *event, void *user) +{ + dirty = true; + switch(event->type) + { + case SETSU_EVENT_DEVICE_ADDED: { + if(event->dev_type != SETSU_DEVICE_TYPE_MOTION) + break; + SetsuDevice *dev = setsu_connect(setsu, event->path, SETSU_DEVICE_TYPE_MOTION); + LOG("Device added: %s, connect %s\n", event->path, dev ? "succeeded" : "FAILED!"); + break; + } + case SETSU_EVENT_DEVICE_REMOVED: + if(event->dev_type != SETSU_DEVICE_TYPE_MOTION) + break; + LOG("Device removed: %s\n", event->path); + break; + // TODO: motion events + default: + break; + } +} + +void usage(const char *prog) +{ + printf("usage: %s [-l]\n -l log mode\n", prog); + exit(1); +} + +int main(int argc, const char *argv[]) +{ + log_mode = false; + if(argc == 2) + { + if(!strcmp(argv[1], "-l")) + log_mode = true; + else + usage(argv[0]); + } + else if(argc != 1) + usage(argv[0]); + + setsu = setsu_new(); + if(!setsu) + { + printf("Failed to init setsu\n"); + return 1; + } + + struct sigaction sa = {0}; + sa.sa_handler = sigint; + sigemptyset(&sa.sa_mask); + sigaction(SIGINT, &sa, NULL); + + dirty = true; + while(!should_quit) + { + if(dirty && !log_mode) + print_state(); + dirty = false; + setsu_poll(setsu, event, NULL); + } + setsu_free(setsu); + printf("\nさよなら!\n"); + return 0; +} + diff --git a/setsu/demo/touchpad.c b/setsu/demo/touchpad.c index 5f7deff..18757d4 100644 --- a/setsu/demo/touchpad.c +++ b/setsu/demo/touchpad.c @@ -122,6 +122,8 @@ void event(SetsuEvent *event, void *user) LOG("Button for %s: %llu %s\n", setsu_device_get_path(event->dev), (unsigned long long)event->button, event->type == SETSU_EVENT_BUTTON_DOWN ? "down" : "up"); break; + default: + break; } } diff --git a/setsu/src/setsu.c b/setsu/src/setsu.c index 520de61..85bd41b 100644 --- a/setsu/src/setsu.c +++ b/setsu/src/setsu.c @@ -178,13 +178,18 @@ static bool is_device_interesting(struct udev_device *dev, SetsuDeviceType *type const char *accel_str = udev_device_get_property_value(dev, "ID_INPUT_ACCELEROMETER"); if(touchpad_str && !strcmp(touchpad_str, "1")) { - // Filter mouse-device (/dev/input/mouse*) away and only keep the evdev (/dev/input/event*) one: + // Filter mouse-device (/dev/input/mouse*) away and only keep the evdev (/dev/input/event*) one: if(!udev_device_get_property_value(dev, "ID_INPUT_TOUCHPAD_INTEGRATION")) return false; *type = SETSU_DEVICE_TYPE_TOUCHPAD; } - else if(touchpad_str && !strcmp(touchpad_str, "1")) + else if(accel_str && !strcmp(accel_str, "1")) + { + // Filter /dev/input/js* away and keep /dev/input/event* + if(!udev_device_get_property_value(dev, "ID_INPUT_WIDTH_MM")) + return false; *type = SETSU_DEVICE_TYPE_MOTION; + } else return false; @@ -435,6 +440,7 @@ void setsu_poll(Setsu *setsu, SetsuEventCb cb, void *user) SetsuEvent event = { 0 }; event.type = SETSU_EVENT_DEVICE_ADDED; event.path = adev->path; + event.dev_type = adev->type; cb(&event, user); adev->connect_dirty = false; } @@ -443,6 +449,7 @@ void setsu_poll(Setsu *setsu, SetsuEventCb cb, void *user) SetsuEvent event = { 0 }; event.type = SETSU_EVENT_DEVICE_REMOVED; event.path = adev->path; + event.dev_type = adev->type; cb(&event, user); // kill the device only after sending the event SetsuAvailDevice *next = adev->next; From 88c03aa7441ff20a1b81ce82d33de01f9ec8ef6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Wed, 6 Jan 2021 14:23:26 +0100 Subject: [PATCH 156/237] Finish Motion in Setsu --- gui/src/streamsession.cpp | 8 ++-- setsu/demo/motion.c | 72 +++++++++++++++++++++++++++++++++--- setsu/demo/touchpad.c | 14 +++---- setsu/include/setsu.h | 14 ++++++- setsu/src/setsu.c | 78 +++++++++++++++++++++++++++++++++++---- 5 files changed, 160 insertions(+), 26 deletions(-) diff --git a/gui/src/streamsession.cpp b/gui/src/streamsession.cpp index 52915f0..97a1305 100644 --- a/gui/src/streamsession.cpp +++ b/gui/src/streamsession.cpp @@ -429,7 +429,7 @@ void StreamSession::HandleSetsuEvent(SetsuEvent *event) case SETSU_EVENT_TOUCH_UP: for(auto it=setsu_ids.begin(); it!=setsu_ids.end(); it++) { - if(it.key().first == setsu_device_get_path(event->dev) && it.key().second == event->tracking_id) + if(it.key().first == setsu_device_get_path(event->dev) && it.key().second == event->touch.tracking_id) { chiaki_controller_state_stop_touch(&setsu_state, it.value()); setsu_ids.erase(it); @@ -439,18 +439,18 @@ void StreamSession::HandleSetsuEvent(SetsuEvent *event) SendFeedbackState(); break; case SETSU_EVENT_TOUCH_POSITION: { - QPair k = { setsu_device_get_path(event->dev), event->tracking_id }; + QPair k = { setsu_device_get_path(event->dev), event->touch.tracking_id }; auto it = setsu_ids.find(k); if(it == setsu_ids.end()) { - int8_t cid = chiaki_controller_state_start_touch(&setsu_state, event->x, event->y); + int8_t cid = chiaki_controller_state_start_touch(&setsu_state, event->touch.x, event->touch.y); if(cid >= 0) setsu_ids[k] = (uint8_t)cid; else break; } else - chiaki_controller_state_set_touch_pos(&setsu_state, it.value(), event->x, event->y); + chiaki_controller_state_set_touch_pos(&setsu_state, it.value(), event->touch.x, event->touch.y); SendFeedbackState(); break; } diff --git a/setsu/demo/motion.c b/setsu/demo/motion.c index f66e691..0b0f88b 100644 --- a/setsu/demo/motion.c +++ b/setsu/demo/motion.c @@ -8,9 +8,29 @@ #include #include #include +#include Setsu *setsu; +#define NAME_LEN 8 +const char * const names[] = { + "accel x ", + "accel y ", + "accel z ", + " gyro x ", + " gyro y ", + " gyro z " +}; +union +{ + struct + { + float accel_x, accel_y, accel_z; + float gyro_x, gyro_y, gyro_z; + }; + float v[6]; +} vals; +uint32_t timestamp; bool dirty = false; bool log_mode; volatile bool should_quit; @@ -22,14 +42,44 @@ void sigint(int s) should_quit = true; } +#define BAR_LENGTH 100 +#define BAR_MAX 2.0f +#define BAR_MAX_GYRO 180.0f + void print_state() { -#if 0 - char buf[256]; - *buf = 0; + char buf[6 * (1 + NAME_LEN + BAR_LENGTH) + 1]; + size_t i = 0; + for(size_t b=0; b<6; b++) + { + buf[i++] = '\n'; + memcpy(buf + i, names[b], NAME_LEN); + i += NAME_LEN; + buf[i++] = '['; + size_t max = BAR_LENGTH-2; + for(size_t bi=0; bi 2 ? BAR_MAX_GYRO : BAR_MAX) * (2.0f * (float)(x) / (float)max - 1.0f)) + float cur = BAR_VAL(bi); + float prev = BAR_VAL((int)bi - 1); + if(prev < 0.0f && cur >= 0.0f) + { + buf[i++] = '|'; + continue; + } + bool cov = ((vals.v[b] < 0.0f) == (cur < 0.0f)) && fabsf(vals.v[b]) > fabsf(cur); + float next = BAR_VAL(bi + 1); +#define MARK_VAL (b > 2 ? 90.0f : 1.0f) + bool mark = cur < -MARK_VAL && next >= -MARK_VAL || prev < MARK_VAL && cur >= MARK_VAL; + buf[i++] = cov ? (mark ? '#' : '=') : (mark ? '.' : ' '); +#undef BAR_VAL + } + buf[i++] = ']'; + } + buf[i++] = '\0'; + assert(i == sizeof(buf)); printf("\033[2J%s", buf); fflush(stdout); -#endif } void event(SetsuEvent *event, void *user) @@ -49,7 +99,19 @@ void event(SetsuEvent *event, void *user) break; LOG("Device removed: %s\n", event->path); break; - // TODO: motion events + case SETSU_EVENT_MOTION: + LOG("Motion: %f, %f, %f / %f, %f, %f / %u\n", + event->motion.accel_x, event->motion.accel_y, event->motion.accel_z, + event->motion.gyro_x, event->motion.gyro_y, event->motion.gyro_z, + (unsigned int)event->motion.timestamp); + vals.accel_x = event->motion.accel_x; + vals.accel_y = event->motion.accel_y; + vals.accel_z = event->motion.accel_z; + vals.gyro_x = event->motion.gyro_x; + vals.gyro_y = event->motion.gyro_y; + vals.gyro_z = event->motion.gyro_z; + timestamp = event->motion.timestamp; + dirty = true; default: break; } diff --git a/setsu/demo/touchpad.c b/setsu/demo/touchpad.c index 18757d4..b51d628 100644 --- a/setsu/demo/touchpad.c +++ b/setsu/demo/touchpad.c @@ -80,13 +80,13 @@ void event(SetsuEvent *event, void *user) LOG("Device removed: %s\n", event->path); break; case SETSU_EVENT_TOUCH_DOWN: - LOG("Down for %s, tracking id %d\n", setsu_device_get_path(event->dev), event->tracking_id); + LOG("Down for %s, tracking id %d\n", setsu_device_get_path(event->dev), event->touch.tracking_id); for(size_t i=0; itracking_id; + touches[i].tracking_id = event->touch.tracking_id; break; } } @@ -94,19 +94,19 @@ void event(SetsuEvent *event, void *user) case SETSU_EVENT_TOUCH_POSITION: case SETSU_EVENT_TOUCH_UP: if(event->type == SETSU_EVENT_TOUCH_UP) - LOG("Up for %s, tracking id %d\n", setsu_device_get_path(event->dev), event->tracking_id); + LOG("Up for %s, tracking id %d\n", setsu_device_get_path(event->dev), event->touch.tracking_id); else LOG("Position for %s, tracking id %d: %u, %u\n", setsu_device_get_path(event->dev), - event->tracking_id, (unsigned int)event->x, (unsigned int)event->y); + event->touch.tracking_id, (unsigned int)event->touch.x, (unsigned int)event->touch.y); for(size_t i=0; itracking_id) + if(touches[i].down && touches[i].tracking_id == event->touch.tracking_id) { switch(event->type) { case SETSU_EVENT_TOUCH_POSITION: - touches[i].x = event->x; - touches[i].y = event->y; + touches[i].x = event->touch.x; + touches[i].y = event->touch.y; break; case SETSU_EVENT_TOUCH_UP: touches[i].down = false; diff --git a/setsu/include/setsu.h b/setsu/include/setsu.h index a2ca4ed..36ecf03 100644 --- a/setsu/include/setsu.h +++ b/setsu/include/setsu.h @@ -46,7 +46,10 @@ typedef enum { SETSU_EVENT_BUTTON_DOWN, /* Event will have dev and button set. */ - SETSU_EVENT_BUTTON_UP + SETSU_EVENT_BUTTON_UP, + + /* Event will have motion set. */ + SETSU_EVENT_MOTION } SetsuEventType; #define SETSU_BUTTON_0 (1u << 0) @@ -72,8 +75,14 @@ typedef struct setsu_event_t { SetsuTrackingId tracking_id; uint32_t x, y; - }; + } touch; SetsuButton button; + struct + { + float accel_x, accel_y, accel_z; // unit is 1G + float gyro_x, gyro_y, gyro_z; // unit is deg/sec + uint32_t timestamp; // microseconds + } motion; }; }; }; @@ -90,6 +99,7 @@ const char *setsu_device_get_path(SetsuDevice *dev); uint32_t setsu_device_touchpad_get_width(SetsuDevice *dev); uint32_t setsu_device_touchpad_get_height(SetsuDevice *dev); + #ifdef __cplusplus } #endif diff --git a/setsu/src/setsu.c b/setsu/src/setsu.c index 85bd41b..6395313 100644 --- a/setsu/src/setsu.c +++ b/setsu/src/setsu.c @@ -67,6 +67,12 @@ typedef struct setsu_device_t } touchpad; struct { + int accel_res_x, accel_res_y, accel_res_z; + int gyro_res_x, gyro_res_y, gyro_res_z; + int accel_x, accel_y, accel_z; + int gyro_x, gyro_y, gyro_z; + uint32_t timestamp; + bool dirty; } motion; }; } SetsuDevice; @@ -341,7 +347,13 @@ SetsuDevice *setsu_connect(Setsu *setsu, const char *path, SetsuDeviceType type) } break; case SETSU_DEVICE_TYPE_MOTION: - // TODO: init to defaults + dev->motion.accel_res_x = libevdev_get_abs_resolution(dev->evdev, ABS_X); + dev->motion.accel_res_y = libevdev_get_abs_resolution(dev->evdev, ABS_Y); + dev->motion.accel_res_z = libevdev_get_abs_resolution(dev->evdev, ABS_Z); + dev->motion.gyro_res_x = libevdev_get_abs_resolution(dev->evdev, ABS_RX); + dev->motion.gyro_res_y = libevdev_get_abs_resolution(dev->evdev, ABS_RY); + dev->motion.gyro_res_z = libevdev_get_abs_resolution(dev->evdev, ABS_RZ); + dev->motion.accel_y = dev->motion.accel_res_y; // 1G down break; } @@ -573,7 +585,45 @@ static void device_event(Setsu *setsu, SetsuDevice *dev, struct input_event *ev, } break; case SETSU_DEVICE_TYPE_MOTION: - // TODO: handle the events + switch(ev->type) + { + case EV_ABS: + switch(ev->code) + { + case ABS_X: + dev->motion.accel_x = ev->value; + dev->motion.dirty = true; + break; + case ABS_Y: + dev->motion.accel_y = ev->value; + dev->motion.dirty = true; + break; + case ABS_Z: + dev->motion.accel_z = ev->value; + dev->motion.dirty = true; + break; + case ABS_RX: + dev->motion.gyro_x = ev->value; + dev->motion.dirty = true; + break; + case ABS_RY: + dev->motion.gyro_y = ev->value; + dev->motion.dirty = true; + break; + case ABS_RZ: + dev->motion.gyro_z = ev->value; + dev->motion.dirty = true; + break; + } + break; + case EV_MSC: + if(ev->code == MSC_TIMESTAMP) + { + dev->motion.timestamp = ev->value; + dev->motion.dirty = true; + } + break; + } break; } } @@ -591,23 +641,23 @@ static void device_drain(Setsu *setsu, SetsuDevice *dev, SetsuEventCb cb, void * if(dev->touchpad.slots[i].tracking_id_prev != -1) { BEGIN_EVENT(SETSU_EVENT_TOUCH_UP); - event.tracking_id = dev->touchpad.slots[i].tracking_id_prev; + event.touch.tracking_id = dev->touchpad.slots[i].tracking_id_prev; SEND_EVENT(); dev->touchpad.slots[i].tracking_id_prev = -1; } if(dev->touchpad.slots[i].downed) { BEGIN_EVENT(SETSU_EVENT_TOUCH_DOWN); - event.tracking_id = dev->touchpad.slots[i].tracking_id; + event.touch.tracking_id = dev->touchpad.slots[i].tracking_id; SEND_EVENT(); dev->touchpad.slots[i].downed = false; } if(dev->touchpad.slots[i].pos_dirty) { BEGIN_EVENT(SETSU_EVENT_TOUCH_POSITION); - event.tracking_id = dev->touchpad.slots[i].tracking_id; - event.x = (uint32_t)(dev->touchpad.slots[i].x - dev->touchpad.min_x); - event.y = (uint32_t)(dev->touchpad.slots[i].y - dev->touchpad.min_y); + event.touch.tracking_id = dev->touchpad.slots[i].tracking_id; + event.touch.x = (uint32_t)(dev->touchpad.slots[i].x - dev->touchpad.min_x); + event.touch.y = (uint32_t)(dev->touchpad.slots[i].y - dev->touchpad.min_y); SEND_EVENT(); dev->touchpad.slots[i].pos_dirty = false; } @@ -630,7 +680,19 @@ static void device_drain(Setsu *setsu, SetsuDevice *dev, SetsuEventCb cb, void * dev->touchpad.buttons_prev = dev->touchpad.buttons_cur; break; case SETSU_DEVICE_TYPE_MOTION: - // TODO + if(dev->motion.dirty) + { + BEGIN_EVENT(SETSU_EVENT_MOTION); + event.motion.accel_x = (float)dev->motion.accel_x / (float)dev->motion.accel_res_x; + event.motion.accel_y = (float)dev->motion.accel_y / (float)dev->motion.accel_res_y; + event.motion.accel_z = (float)dev->motion.accel_z / (float)dev->motion.accel_res_z; + event.motion.gyro_x = (float)dev->motion.gyro_x / (float)dev->motion.gyro_res_x; + event.motion.gyro_y = (float)dev->motion.gyro_y / (float)dev->motion.gyro_res_y; + event.motion.gyro_z = (float)dev->motion.gyro_z / (float)dev->motion.gyro_res_z; + event.motion.timestamp = dev->motion.timestamp; + SEND_EVENT(); + dev->motion.dirty = false; + } break; } #undef BEGIN_EVENT From 32e1539c2201b1c46856919543b716f5be1affa9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Wed, 6 Jan 2021 15:47:42 +0100 Subject: [PATCH 157/237] Add Orientation Tracker --- lib/CMakeLists.txt | 6 +- lib/include/chiaki/orientation.h | 43 +++++++++++ lib/src/orientation.c | 126 +++++++++++++++++++++++++++++++ setsu/demo/motion.c | 4 +- setsu/include/setsu.h | 2 +- setsu/src/setsu.c | 9 ++- 6 files changed, 182 insertions(+), 8 deletions(-) create mode 100644 lib/include/chiaki/orientation.h create mode 100644 lib/src/orientation.c diff --git a/lib/CMakeLists.txt b/lib/CMakeLists.txt index e2a79d3..36ac38c 100644 --- a/lib/CMakeLists.txt +++ b/lib/CMakeLists.txt @@ -35,7 +35,8 @@ set(HEADER_FILES include/chiaki/time.h include/chiaki/fec.h include/chiaki/regist.h - include/chiaki/opusdecoder.h) + include/chiaki/opusdecoder.h + include/chiaki/orientation.h) set(SOURCE_FILES src/common.c @@ -73,7 +74,8 @@ set(SOURCE_FILES src/time.c src/fec src/regist.c - src/opusdecoder.c) + src/opusdecoder.c + src/orientation.c) if(CHIAKI_ENABLE_FFMPEG_DECODER) list(APPEND HEADER_FILES include/chiaki/ffmpegdecoder.h) diff --git a/lib/include/chiaki/orientation.h b/lib/include/chiaki/orientation.h new file mode 100644 index 0000000..4a29c2e --- /dev/null +++ b/lib/include/chiaki/orientation.h @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL + +#ifndef CHIAKI_ORIENTATION_H +#define CHIAKI_ORIENTATION_H + +#include "common.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * Quaternion orientation from accelerometer and gyroscope + * using Madgwick's algorithm. + * See: http://www.x-io.co.uk/node/8#open_source_ahrs_and_imu_algorithms + */ +typedef struct chiaki_orientation_t +{ + float x, y, z, w; +} ChiakiOrientation; + +CHIAKI_EXPORT void chiaki_orientation_init(ChiakiOrientation *orient); +CHIAKI_EXPORT void chiaki_orientation_update(ChiakiOrientation *orient, float gx, float gy, float gz, float ax, float ay, float az, float time_step_sec); + +/** + * Extension of ChiakiOrientation, also tracking an absolute timestamp + */ +typedef struct chiaki_orientation_tracker_t +{ + ChiakiOrientation orient; + uint32_t timestamp; + bool first_sample; +} ChiakiOrientationTracker; + +CHIAKI_EXPORT void chiaki_orientation_tracker_init(ChiakiOrientationTracker *tracker); +CHIAKI_EXPORT void chiaki_orientation_tracker_update(ChiakiOrientationTracker *tracker, + float gx, float gy, float gz, float ax, float ay, float az, uint32_t timestamp_us); + +#ifdef __cplusplus +} +#endif + +#endif // CHIAKI_ORIENTATION_H diff --git a/lib/src/orientation.c b/lib/src/orientation.c new file mode 100644 index 0000000..90e991f --- /dev/null +++ b/lib/src/orientation.c @@ -0,0 +1,126 @@ +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL + +#include + +CHIAKI_EXPORT void chiaki_orientation_init(ChiakiOrientation *orient) +{ + orient->x = orient->y = orient->z = 0.0f; + orient->w = 1.0f; +} + +#define BETA 0.1f // 2 * proportional gain +static float inv_sqrt(float x); + +CHIAKI_EXPORT void chiaki_orientation_update(ChiakiOrientation *orient, float gx, float gy, float gz, float ax, float ay, float az, float time_step_sec) +{ + float q0 = orient->w, q1 = orient->x, q2 = orient->y, q3 = orient->z; + // Madgwick's IMU algorithm. + // See: http://www.x-io.co.uk/node/8#open_source_ahrs_and_imu_algorithms + float recip_norm; + float s0, s1, s2, s3; + float q_dot1, q_dot2, q_dot3, q_dot4; + float _2q0, _2q1, _2q2, _2q3, _4q0, _4q1, _4q2 ,_8q1, _8q2, q0q0, q1q1, q2q2, q3q3; + + // Rate of change of quaternion from gyroscope + q_dot1 = 0.5f * (-q1 * gx - q2 * gy - q3 * gz); + q_dot2 = 0.5f * (q0 * gx + q2 * gz - q3 * gy); + q_dot3 = 0.5f * (q0 * gy - q1 * gz + q3 * gx); + q_dot4 = 0.5f * (q0 * gz + q1 * gy - q2 * gx); + + // Compute feedback only if accelerometer measurement valid (avoids NaN in accelerometer normalisation) + if(!((ax == 0.0f) && (ay == 0.0f) && (az == 0.0f))) + { + // Normalise accelerometer measurement + recip_norm = inv_sqrt(ax * ax + ay * ay + az * az); + ax *= recip_norm; + ay *= recip_norm; + az *= recip_norm; + + // Auxiliary variables to avoid repeated arithmetic + _2q0 = 2.0f * q0; + _2q1 = 2.0f * q1; + _2q2 = 2.0f * q2; + _2q3 = 2.0f * q3; + _4q0 = 4.0f * q0; + _4q1 = 4.0f * q1; + _4q2 = 4.0f * q2; + _8q1 = 8.0f * q1; + _8q2 = 8.0f * q2; + q0q0 = q0 * q0; + q1q1 = q1 * q1; + q2q2 = q2 * q2; + q3q3 = q3 * q3; + + // Gradient decent algorithm corrective step + s0 = _4q0 * q2q2 + _2q2 * ax + _4q0 * q1q1 - _2q1 * ay; + s1 = _4q1 * q3q3 - _2q3 * ax + 4.0f * q0q0 * q1 - _2q0 * ay - _4q1 + _8q1 * q1q1 + _8q1 * q2q2 + _4q1 * az; + s2 = 4.0f * q0q0 * q2 + _2q0 * ax + _4q2 * q3q3 - _2q3 * ay - _4q2 + _8q2 * q1q1 + _8q2 * q2q2 + _4q2 * az; + s3 = 4.0f * q1q1 * q3 - _2q1 * ax + 4.0f * q2q2 * q3 - _2q2 * ay; + recip_norm = inv_sqrt(s0 * s0 + s1 * s1 + s2 * s2 + s3 * s3); // normalise step magnitude + s0 *= recip_norm; + s1 *= recip_norm; + s2 *= recip_norm; + s3 *= recip_norm; + + // Apply feedback step + q_dot1 -= BETA * s0; + q_dot2 -= BETA * s1; + q_dot3 -= BETA * s2; + q_dot4 -= BETA * s3; + } + + // Integrate rate of change of quaternion to yield quaternion + q0 += q_dot1 * time_step_sec; + q1 += q_dot2 * time_step_sec; + q2 += q_dot3 * time_step_sec; + q3 += q_dot4 * time_step_sec; + + // Normalise quaternion + recip_norm = inv_sqrt(q0 * q0 + q1 * q1 + q2 * q2 + q3 * q3); + q0 *= recip_norm; + q1 *= recip_norm; + q2 *= recip_norm; + q3 *= recip_norm; + + orient->x = q1; + orient->y = q2; + orient->z = q3; + orient->w = q0; +} + +static float inv_sqrt(float x) +{ + // Fast inverse square-root + // See: http://en.wikipedia.org/wiki/Fast_inverse_square_root + float halfx = 0.5f * x; + float y = x; + long i = *(long*)&y; + i = 0x5f3759df - (i>>1); + y = *(float*)&i; + y = y * (1.5f - (halfx * y * y)); + return y; +} + +CHIAKI_EXPORT void chiaki_orientation_tracker_init(ChiakiOrientationTracker *tracker) +{ + chiaki_orientation_init(&tracker->orient); + tracker->timestamp = 0; + tracker->first_sample = true; +} + +CHIAKI_EXPORT void chiaki_orientation_tracker_update(ChiakiOrientationTracker *tracker, + float gx, float gy, float gz, float ax, float ay, float az, uint32_t timestamp_us) +{ + if(tracker->first_sample) + { + tracker->first_sample = false; + tracker->timestamp = timestamp_us; + return; + } + uint64_t delta_us = timestamp_us; + if(delta_us < tracker->timestamp) + delta_us += (1ULL << 32); + delta_us -= tracker->timestamp; + tracker->timestamp = timestamp_us; + chiaki_orientation_update(&tracker->orient, gx, gy, gz, ax, ay, az, (float)delta_us / 1000000.0f); +} diff --git a/setsu/demo/motion.c b/setsu/demo/motion.c index 0b0f88b..ebb5fdd 100644 --- a/setsu/demo/motion.c +++ b/setsu/demo/motion.c @@ -44,7 +44,7 @@ void sigint(int s) #define BAR_LENGTH 100 #define BAR_MAX 2.0f -#define BAR_MAX_GYRO 180.0f +#define BAR_MAX_GYRO M_PI void print_state() { @@ -69,7 +69,7 @@ void print_state() } bool cov = ((vals.v[b] < 0.0f) == (cur < 0.0f)) && fabsf(vals.v[b]) > fabsf(cur); float next = BAR_VAL(bi + 1); -#define MARK_VAL (b > 2 ? 90.0f : 1.0f) +#define MARK_VAL (b > 2 ? 0.5f * M_PI : 1.0f) bool mark = cur < -MARK_VAL && next >= -MARK_VAL || prev < MARK_VAL && cur >= MARK_VAL; buf[i++] = cov ? (mark ? '#' : '=') : (mark ? '.' : ' '); #undef BAR_VAL diff --git a/setsu/include/setsu.h b/setsu/include/setsu.h index 36ecf03..a3ed0e3 100644 --- a/setsu/include/setsu.h +++ b/setsu/include/setsu.h @@ -80,7 +80,7 @@ typedef struct setsu_event_t struct { float accel_x, accel_y, accel_z; // unit is 1G - float gyro_x, gyro_y, gyro_z; // unit is deg/sec + float gyro_x, gyro_y, gyro_z; // unit is rad/sec uint32_t timestamp; // microseconds } motion; }; diff --git a/setsu/src/setsu.c b/setsu/src/setsu.c index 6395313..c31fe83 100644 --- a/setsu/src/setsu.c +++ b/setsu/src/setsu.c @@ -11,6 +11,7 @@ #include #include #include +#include #include @@ -22,6 +23,8 @@ #define SETSU_LOG(...) do {} while(0) #endif +#define DEG2RAD (2.0f * M_PI / 360.0f) + typedef struct setsu_avail_device_t { struct setsu_avail_device_t *next; @@ -686,9 +689,9 @@ static void device_drain(Setsu *setsu, SetsuDevice *dev, SetsuEventCb cb, void * event.motion.accel_x = (float)dev->motion.accel_x / (float)dev->motion.accel_res_x; event.motion.accel_y = (float)dev->motion.accel_y / (float)dev->motion.accel_res_y; event.motion.accel_z = (float)dev->motion.accel_z / (float)dev->motion.accel_res_z; - event.motion.gyro_x = (float)dev->motion.gyro_x / (float)dev->motion.gyro_res_x; - event.motion.gyro_y = (float)dev->motion.gyro_y / (float)dev->motion.gyro_res_y; - event.motion.gyro_z = (float)dev->motion.gyro_z / (float)dev->motion.gyro_res_z; + event.motion.gyro_x = DEG2RAD * (float)dev->motion.gyro_x / (float)dev->motion.gyro_res_x; + event.motion.gyro_y = DEG2RAD * (float)dev->motion.gyro_y / (float)dev->motion.gyro_res_y; + event.motion.gyro_z = DEG2RAD * (float)dev->motion.gyro_z / (float)dev->motion.gyro_res_z; event.motion.timestamp = dev->motion.timestamp; SEND_EVENT(); dev->motion.dirty = false; From 170dcd4d65c736e6e81a854f174f5d2fb34fe60b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Wed, 6 Jan 2021 16:35:01 +0100 Subject: [PATCH 158/237] Connect and read Setsu Motion Device in GUI --- gui/include/streamsession.h | 4 +++ gui/src/streamsession.cpp | 72 +++++++++++++++++++++++++++++++------ 2 files changed, 66 insertions(+), 10 deletions(-) diff --git a/gui/include/streamsession.h b/gui/include/streamsession.h index 47dc217..cb1397e 100644 --- a/gui/include/streamsession.h +++ b/gui/include/streamsession.h @@ -13,6 +13,7 @@ #if CHIAKI_GUI_ENABLE_SETSU #include +#include #endif #include "exception.h" @@ -74,6 +75,9 @@ class StreamSession : public QObject Setsu *setsu; QMap, uint8_t> setsu_ids; ChiakiControllerState setsu_state; + SetsuDevice *setsu_motion_device; + ChiakiOrientationTracker orient_tracker; + bool orient_dirty; #endif ChiakiControllerState keyboard_state; diff --git a/gui/src/streamsession.cpp b/gui/src/streamsession.cpp index 97a1305..dcec36d 100644 --- a/gui/src/streamsession.cpp +++ b/gui/src/streamsession.cpp @@ -153,11 +153,20 @@ StreamSession::StreamSession(const StreamSessionConnectInfo &connect_info, QObje #endif #if CHIAKI_GUI_ENABLE_SETSU + setsu_motion_device = nullptr; chiaki_controller_state_set_idle(&setsu_state); + orient_dirty = false; + chiaki_orientation_tracker_init(&orient_tracker); setsu = setsu_new(); auto timer = new QTimer(this); connect(timer, &QTimer::timeout, this, [this]{ setsu_poll(setsu, SessionSetsuCb, this); + if(orient_dirty) + { + // TODO: put orient/gyro/acc into setsu_state + // and SendFeedbackState(); + orient_dirty = false; + } }); timer->start(SETSU_UPDATE_INTERVAL_MS); #endif @@ -409,20 +418,56 @@ void StreamSession::HandleSetsuEvent(SetsuEvent *event) switch(event->type) { case SETSU_EVENT_DEVICE_ADDED: - setsu_connect(setsu, event->path, event->dev_type); + switch(event->dev_type) + { + case SETSU_DEVICE_TYPE_TOUCHPAD: + // connect all the touchpads! + if(setsu_connect(setsu, event->path, event->dev_type)) + CHIAKI_LOGI(GetChiakiLog(), "Connected Setsu Touchpad Device %s", event->path); + else + CHIAKI_LOGE(GetChiakiLog(), "Failed to connect to Setsu Touchpad Device %s", event->path); + break; + case SETSU_DEVICE_TYPE_MOTION: + // connect only one motion since multiple make no sense + if(setsu_motion_device) + { + CHIAKI_LOGI(GetChiakiLog(), "Setsu Motion Device %s detected there is already one connected", + event->path); + break; + } + setsu_motion_device = setsu_connect(setsu, event->path, event->dev_type); + if(setsu_motion_device) + CHIAKI_LOGI(GetChiakiLog(), "Connected Setsu Motion Device %s", event->path); + else + CHIAKI_LOGE(GetChiakiLog(), "Failed to connect to Setsu Motion Device %s", event->path); + break; + } break; case SETSU_EVENT_DEVICE_REMOVED: - for(auto it=setsu_ids.begin(); it!=setsu_ids.end();) + switch(event->dev_type) { - if(it.key().first == event->path) - { - chiaki_controller_state_stop_touch(&setsu_state, it.value()); - setsu_ids.erase(it++); - } - else - it++; + case SETSU_DEVICE_TYPE_TOUCHPAD: + CHIAKI_LOGI(GetChiakiLog(), "Setsu Touchpad Device %s disconnected", event->path); + for(auto it=setsu_ids.begin(); it!=setsu_ids.end();) + { + if(it.key().first == event->path) + { + chiaki_controller_state_stop_touch(&setsu_state, it.value()); + setsu_ids.erase(it++); + } + else + it++; + } + SendFeedbackState(); + break; + case SETSU_DEVICE_TYPE_MOTION: + if(!setsu_motion_device || strcmp(setsu_device_get_path(setsu_motion_device), event->path)) + break; + CHIAKI_LOGI(GetChiakiLog(), "Setsu Motion Device %s disconnected", event->path); + setsu_motion_device = nullptr; + SendFeedbackState(); + break; } - SendFeedbackState(); break; case SETSU_EVENT_TOUCH_DOWN: break; @@ -460,6 +505,13 @@ void StreamSession::HandleSetsuEvent(SetsuEvent *event) case SETSU_EVENT_BUTTON_UP: setsu_state.buttons &= ~CHIAKI_CONTROLLER_BUTTON_TOUCHPAD; break; + case SETSU_EVENT_MOTION: + chiaki_orientation_tracker_update(&orient_tracker, + event->motion.gyro_x, event->motion.gyro_y, event->motion.gyro_z, + event->motion.accel_x, event->motion.accel_y, event->motion.accel_z, + event->motion.timestamp); + orient_dirty = true; + break; } } #endif From cb827a525a1076b66371510e12e8605e48660bb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Wed, 6 Jan 2021 21:42:06 +0100 Subject: [PATCH 159/237] Add Motion to Feedback --- gui/src/streamsession.cpp | 18 ++++---- lib/include/chiaki/controller.h | 29 ++++--------- lib/include/chiaki/feedback.h | 3 ++ lib/include/chiaki/orientation.h | 10 ++++- lib/src/controller.c | 43 +++++++++++++++++++ lib/src/feedback.c | 73 ++++++++++++++++++++++++-------- lib/src/feedbacksender.c | 28 +++++++++++- lib/src/orientation.c | 28 +++++++++++- 8 files changed, 182 insertions(+), 50 deletions(-) diff --git a/gui/src/streamsession.cpp b/gui/src/streamsession.cpp index dcec36d..7d25136 100644 --- a/gui/src/streamsession.cpp +++ b/gui/src/streamsession.cpp @@ -155,7 +155,7 @@ StreamSession::StreamSession(const StreamSessionConnectInfo &connect_info, QObje #if CHIAKI_GUI_ENABLE_SETSU setsu_motion_device = nullptr; chiaki_controller_state_set_idle(&setsu_state); - orient_dirty = false; + orient_dirty = true; chiaki_orientation_tracker_init(&orient_tracker); setsu = setsu_new(); auto timer = new QTimer(this); @@ -163,8 +163,8 @@ StreamSession::StreamSession(const StreamSessionConnectInfo &connect_info, QObje setsu_poll(setsu, SessionSetsuCb, this); if(orient_dirty) { - // TODO: put orient/gyro/acc into setsu_state - // and SendFeedbackState(); + chiaki_orientation_tracker_apply_to_controller_state(&orient_tracker, &setsu_state); + SendFeedbackState(); orient_dirty = false; } }); @@ -330,16 +330,17 @@ void StreamSession::SendFeedbackState() ChiakiControllerState state; chiaki_controller_state_set_idle(&state); +#if CHIAKI_GUI_ENABLE_SETSU + // setsu is the one that potentially has gyro/accel/orient so copy that directly first + state = setsu_state; +#endif + for(auto controller : controllers) { auto controller_state = controller->GetState(); chiaki_controller_state_or(&state, &state, &controller_state); } -#if CHIAKI_GUI_ENABLE_SETSU - chiaki_controller_state_or(&state, &state, &setsu_state); -#endif - chiaki_controller_state_or(&state, &state, &keyboard_state); chiaki_session_set_controller_state(&session, &state); } @@ -465,7 +466,8 @@ void StreamSession::HandleSetsuEvent(SetsuEvent *event) break; CHIAKI_LOGI(GetChiakiLog(), "Setsu Motion Device %s disconnected", event->path); setsu_motion_device = nullptr; - SendFeedbackState(); + chiaki_orientation_tracker_init(&orient_tracker); + orient_dirty = true; break; } break; diff --git a/lib/include/chiaki/controller.h b/lib/include/chiaki/controller.h index e8f2e7c..24e86a9 100644 --- a/lib/include/chiaki/controller.h +++ b/lib/include/chiaki/controller.h @@ -66,6 +66,10 @@ typedef struct chiaki_controller_state_t uint8_t touch_id_next; ChiakiControllerTouch touches[CHIAKI_CONTROLLER_TOUCHES_MAX]; + + float gyro_x, gyro_y, gyro_z; + float accel_x, accel_y, accel_z; + float orient_x, orient_y, orient_z, orient_w; } ChiakiControllerState; CHIAKI_EXPORT void chiaki_controller_state_set_idle(ChiakiControllerState *state); @@ -79,27 +83,12 @@ CHIAKI_EXPORT void chiaki_controller_state_stop_touch(ChiakiControllerState *sta CHIAKI_EXPORT void chiaki_controller_state_set_touch_pos(ChiakiControllerState *state, uint8_t id, uint16_t x, uint16_t y); -static inline bool chiaki_controller_state_equals(ChiakiControllerState *a, ChiakiControllerState *b) -{ - if(!(a->buttons == b->buttons - && a->l2_state == b->l2_state - && a->r2_state == b->r2_state - && a->left_x == b->left_x - && a->left_y == b->left_y - && a->right_x == b->right_x - && a->right_y == b->right_y)) - return false; - - for(size_t i=0; itouches[i].id != b->touches[i].id) - return false; - if(a->touches[i].id >= 0 && (a->touches[i].x != b->touches[i].x || a->touches[i].y != b->touches[i].y)) - return false; - } - return true; -} +CHIAKI_EXPORT bool chiaki_controller_state_equals(ChiakiControllerState *a, ChiakiControllerState *b); +/** + * Union of two controller states. + * Ignores gyro, accel and orient because it makes no sense there. + */ CHIAKI_EXPORT void chiaki_controller_state_or(ChiakiControllerState *out, ChiakiControllerState *a, ChiakiControllerState *b); #ifdef __cplusplus diff --git a/lib/include/chiaki/feedback.h b/lib/include/chiaki/feedback.h index 264af5c..be66384 100644 --- a/lib/include/chiaki/feedback.h +++ b/lib/include/chiaki/feedback.h @@ -15,6 +15,9 @@ extern "C" { typedef struct chiaki_feedback_state_t { + float gyro_x, gyro_y, gyro_z; + float accel_x, accel_y, accel_z; + float orient_x, orient_y, orient_z, orient_w; int16_t left_x; int16_t left_y; int16_t right_x; diff --git a/lib/include/chiaki/orientation.h b/lib/include/chiaki/orientation.h index 4a29c2e..2322d55 100644 --- a/lib/include/chiaki/orientation.h +++ b/lib/include/chiaki/orientation.h @@ -4,6 +4,7 @@ #define CHIAKI_ORIENTATION_H #include "common.h" +#include "controller.h" #ifdef __cplusplus extern "C" { @@ -20,13 +21,16 @@ typedef struct chiaki_orientation_t } ChiakiOrientation; CHIAKI_EXPORT void chiaki_orientation_init(ChiakiOrientation *orient); -CHIAKI_EXPORT void chiaki_orientation_update(ChiakiOrientation *orient, float gx, float gy, float gz, float ax, float ay, float az, float time_step_sec); +CHIAKI_EXPORT void chiaki_orientation_update(ChiakiOrientation *orient, + float gx, float gy, float gz, float ax, float ay, float az, float time_step_sec); /** - * Extension of ChiakiOrientation, also tracking an absolute timestamp + * Extension of ChiakiOrientation, also tracking an absolute timestamp and the current gyro/accel state */ typedef struct chiaki_orientation_tracker_t { + float gyro_x, gyro_y, gyro_z; + float accel_x, accel_y, accel_z; ChiakiOrientation orient; uint32_t timestamp; bool first_sample; @@ -35,6 +39,8 @@ typedef struct chiaki_orientation_tracker_t CHIAKI_EXPORT void chiaki_orientation_tracker_init(ChiakiOrientationTracker *tracker); CHIAKI_EXPORT void chiaki_orientation_tracker_update(ChiakiOrientationTracker *tracker, float gx, float gy, float gz, float ax, float ay, float az, uint32_t timestamp_us); +CHIAKI_EXPORT void chiaki_orientation_tracker_apply_to_controller_state(ChiakiOrientationTracker *tracker, + ChiakiControllerState *state); #ifdef __cplusplus } diff --git a/lib/src/controller.c b/lib/src/controller.c index 0e3a522..21b9971 100644 --- a/lib/src/controller.c +++ b/lib/src/controller.c @@ -20,6 +20,14 @@ CHIAKI_EXPORT void chiaki_controller_state_set_idle(ChiakiControllerState *state state->touches[i].x = 0; state->touches[i].y = 0; } + state->gyro_x = state->gyro_y = state->gyro_z = 0.0f; + state->accel_x = 0.0f; + state->accel_y = 1.0f; + state->accel_z = 0.0f; + state->orient_x = 0.0f; + state->orient_y = 0.0f; + state->orient_z = 0.0f; + state->orient_w = 1.0f; } CHIAKI_EXPORT int8_t chiaki_controller_state_start_touch(ChiakiControllerState *state, uint16_t x, uint16_t y) @@ -64,6 +72,41 @@ CHIAKI_EXPORT void chiaki_controller_state_set_touch_pos(ChiakiControllerState * } } +CHIAKI_EXPORT bool chiaki_controller_state_equals(ChiakiControllerState *a, ChiakiControllerState *b) +{ + if(!(a->buttons == b->buttons + && a->l2_state == b->l2_state + && a->r2_state == b->r2_state + && a->left_x == b->left_x + && a->left_y == b->left_y + && a->right_x == b->right_x + && a->right_y == b->right_y)) + return false; + + for(size_t i=0; itouches[i].id != b->touches[i].id) + return false; + if(a->touches[i].id >= 0 && (a->touches[i].x != b->touches[i].x || a->touches[i].y != b->touches[i].y)) + return false; + } + +#define CHECKF(n) if(a->n < b->n - 0.0000001f || a->n > b->n + 0.0000001f) return false + CHECKF(gyro_x); + CHECKF(gyro_y); + CHECKF(gyro_z); + CHECKF(accel_x); + CHECKF(accel_y); + CHECKF(accel_z); + CHECKF(orient_x); + CHECKF(orient_y); + CHECKF(orient_z); + CHECKF(orient_w); +#undef CHECKF + + return true; +} + #define MAX(a, b) ((a) > (b) ? (a) : (b)) #define ABS(a) ((a) > 0 ? (a) : -(a)) #define MAX_ABS(a, b) (ABS(a) > ABS(b) ? (a) : (b)) diff --git a/lib/src/feedback.c b/lib/src/feedback.c index 4c52b72..2ce0b98 100644 --- a/lib/src/feedback.c +++ b/lib/src/feedback.c @@ -9,26 +9,65 @@ #include #endif #include +#include + +#define GYRO_MIN -30.0f +#define GYRO_MAX 30.0f +#define ACCEL_MIN -5.0f +#define ACCEL_MAX 5.0f + +static uint32_t compress_quat(float *q) +{ + // very similar idea as https://github.com/jpreiss/quatcompress + size_t largest_i = 0; + for(size_t i = 1; i < 4; i++) + { + if(fabs(q[i]) > fabs(q[largest_i])) + largest_i = i; + } + uint32_t r = (q[largest_i] < 0.0 ? 1 : 0) | (largest_i << 1); + for(size_t i = 0; i < 3; i++) + { + size_t qi = i < largest_i ? i : i + 1; + float v = q[qi]; + if(v < -M_SQRT1_2) + v = -M_SQRT1_2; + if(v > M_SQRT1_2) + v = M_SQRT1_2; + v += M_SQRT1_2; + v *= (float)0x1ff / (2.0f * M_SQRT1_2); + r |= (uint32_t)v << (3 + i * 9); + } + return r; +} CHIAKI_EXPORT void chiaki_feedback_state_format_v9(uint8_t *buf, ChiakiFeedbackState *state) { - buf[0x0] = 0xa0; // TODO - buf[0x1] = 0xff; // TODO - buf[0x2] = 0x7f; // TODO - buf[0x3] = 0xff; // TODO - buf[0x4] = 0x7f; // TODO - buf[0x5] = 0xff; // TODO - buf[0x6] = 0x7f; // TODO - buf[0x7] = 0xff; // TODO - buf[0x8] = 0x7f; // TODO - buf[0x9] = 0x99; // TODO - buf[0xa] = 0x99; // TODO - buf[0xb] = 0xff; // TODO - buf[0xc] = 0x7f; // TODO - buf[0xd] = 0xfe; // TODO - buf[0xe] = 0xf7; // TODO - buf[0xf] = 0xef; // TODO - buf[0x10] = 0x1f; // TODO + buf[0x0] = 0xa0; + uint16_t v = (uint16_t)(0xffff * ((float)state->gyro_x - GYRO_MIN) / (GYRO_MAX - GYRO_MIN)); + buf[0x1] = v; + buf[0x2] = v >> 8; + v = (uint16_t)(0xffff * ((float)state->gyro_y - GYRO_MIN) / (GYRO_MAX - GYRO_MIN)); + buf[0x3] = v; + buf[0x4] = v >> 8; + v = (uint16_t)(0xffff * ((float)state->gyro_z - GYRO_MIN) / (GYRO_MAX - GYRO_MIN)); + buf[0x5] = v; + buf[0x6] = v >> 8; + v = (uint16_t)(0xffff * ((float)state->accel_x - ACCEL_MIN) / (ACCEL_MAX - ACCEL_MIN)); + buf[0x7] = v; + buf[0x8] = v >> 8; + v = (uint16_t)(0xffff * ((float)state->accel_y - ACCEL_MIN) / (ACCEL_MAX - ACCEL_MIN)); + buf[0x9] = v; + buf[0xa] = v >> 8; + v = (uint16_t)(0xffff * ((float)state->accel_z - ACCEL_MIN) / (ACCEL_MAX - ACCEL_MIN)); + buf[0xb] = v; + buf[0xc] = v >> 8; + float q[4] = { state->orient_x, state->orient_y, state->orient_z, state->orient_w }; + uint32_t qc = compress_quat(q); + buf[0xd] = qc; + buf[0xe] = qc >> 0x8; + buf[0xf] = qc >> 0x10; + buf[0x10] = qc >> 0x18; *((chiaki_unaligned_uint16_t *)(buf + 0x11)) = htons((uint16_t)state->left_x); *((chiaki_unaligned_uint16_t *)(buf + 0x13)) = htons((uint16_t)state->left_y); *((chiaki_unaligned_uint16_t *)(buf + 0x15)) = htons((uint16_t)state->right_x); diff --git a/lib/src/feedbacksender.c b/lib/src/feedbacksender.c index dc67bf6..771796f 100644 --- a/lib/src/feedbacksender.c +++ b/lib/src/feedbacksender.c @@ -83,10 +83,24 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_feedback_sender_set_controller_state(Chiaki static bool controller_state_equals_for_feedback_state(ChiakiControllerState *a, ChiakiControllerState *b) { - return a->left_x == b->left_x + if(!(a->left_x == b->left_x && a->left_y == b->left_y && a->right_x == b->right_x - && a->right_y == b->right_y; + && a->right_y == b->right_y)) + return false; +#define CHECKF(n) if(a->n < b->n - 0.0000001f || a->n > b->n + 0.0000001f) return false + CHECKF(gyro_x); + CHECKF(gyro_y); + CHECKF(gyro_z); + CHECKF(accel_x); + CHECKF(accel_y); + CHECKF(accel_z); + CHECKF(orient_x); + CHECKF(orient_y); + CHECKF(orient_z); + CHECKF(orient_w); +#undef CHECKF + return true; } static void feedback_sender_send_state(ChiakiFeedbackSender *feedback_sender) @@ -96,6 +110,16 @@ static void feedback_sender_send_state(ChiakiFeedbackSender *feedback_sender) state.left_y = feedback_sender->controller_state.left_y; state.right_x = feedback_sender->controller_state.right_x; state.right_y = feedback_sender->controller_state.right_y; + state.gyro_x = feedback_sender->controller_state.gyro_x; + state.gyro_y = feedback_sender->controller_state.gyro_y; + state.gyro_z = feedback_sender->controller_state.gyro_z; + state.accel_x = feedback_sender->controller_state.accel_x; + state.accel_y = feedback_sender->controller_state.accel_y; + state.accel_z = feedback_sender->controller_state.accel_z; + state.orient_x = feedback_sender->controller_state.orient_x; + state.orient_y = feedback_sender->controller_state.orient_y; + state.orient_z = feedback_sender->controller_state.orient_z; + state.orient_w = feedback_sender->controller_state.orient_w; ChiakiErrorCode err = chiaki_takion_send_feedback_state(feedback_sender->takion, feedback_sender->state_seq_num++, &state); if(err != CHIAKI_ERR_SUCCESS) CHIAKI_LOGE(feedback_sender->log, "FeedbackSender failed to send Feedback State"); diff --git a/lib/src/orientation.c b/lib/src/orientation.c index 90e991f..bb0849e 100644 --- a/lib/src/orientation.c +++ b/lib/src/orientation.c @@ -11,7 +11,8 @@ CHIAKI_EXPORT void chiaki_orientation_init(ChiakiOrientation *orient) #define BETA 0.1f // 2 * proportional gain static float inv_sqrt(float x); -CHIAKI_EXPORT void chiaki_orientation_update(ChiakiOrientation *orient, float gx, float gy, float gz, float ax, float ay, float az, float time_step_sec) +CHIAKI_EXPORT void chiaki_orientation_update(ChiakiOrientation *orient, + float gx, float gy, float gz, float ax, float ay, float az, float time_step_sec) { float q0 = orient->w, q1 = orient->x, q2 = orient->y, q3 = orient->z; // Madgwick's IMU algorithm. @@ -103,6 +104,10 @@ static float inv_sqrt(float x) CHIAKI_EXPORT void chiaki_orientation_tracker_init(ChiakiOrientationTracker *tracker) { + tracker->accel_x = 0.0f; + tracker->accel_y = 1.0f; + tracker->accel_z = 0.0f; + tracker->gyro_x = tracker->gyro_y = tracker->gyro_z = 0.0f; chiaki_orientation_init(&tracker->orient); tracker->timestamp = 0; tracker->first_sample = true; @@ -111,6 +116,12 @@ CHIAKI_EXPORT void chiaki_orientation_tracker_init(ChiakiOrientationTracker *tra CHIAKI_EXPORT void chiaki_orientation_tracker_update(ChiakiOrientationTracker *tracker, float gx, float gy, float gz, float ax, float ay, float az, uint32_t timestamp_us) { + tracker->gyro_x = gx; + tracker->gyro_y = gy; + tracker->gyro_z = gz; + tracker->accel_x = ax; + tracker->accel_y = ay; + tracker->accel_z = az; if(tracker->first_sample) { tracker->first_sample = false; @@ -124,3 +135,18 @@ CHIAKI_EXPORT void chiaki_orientation_tracker_update(ChiakiOrientationTracker *t tracker->timestamp = timestamp_us; chiaki_orientation_update(&tracker->orient, gx, gy, gz, ax, ay, az, (float)delta_us / 1000000.0f); } + +CHIAKI_EXPORT void chiaki_orientation_tracker_apply_to_controller_state(ChiakiOrientationTracker *tracker, + ChiakiControllerState *state) +{ + state->gyro_x = tracker->gyro_x; + state->gyro_y = tracker->gyro_y; + state->gyro_z = tracker->gyro_z; + state->accel_x = tracker->accel_x; + state->accel_y = tracker->accel_y; + state->accel_z = tracker->accel_z; + state->orient_x = tracker->orient.x; + state->orient_y = tracker->orient.y; + state->orient_z = tracker->orient.z; + state->orient_w = tracker->orient.w; +} From 24d73064db228296468342eecd96f3f19140eb9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Wed, 6 Jan 2021 21:51:18 +0100 Subject: [PATCH 160/237] Format Fixes --- lib/src/rpcrypt.c | 2 +- lib/src/session.c | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/src/rpcrypt.c b/lib/src/rpcrypt.c index 30225c0..15b64d0 100644 --- a/lib/src/rpcrypt.c +++ b/lib/src/rpcrypt.c @@ -1698,7 +1698,7 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_rpcrypt_aeropause(ChiakiTarget target, size CHIAKI_EXPORT void chiaki_rpcrypt_init_auth(ChiakiRPCrypt *rpcrypt, ChiakiTarget target, const uint8_t *nonce, const uint8_t *morning) { rpcrypt->target = target; - chiaki_rpcrypt_bright_ambassador(target, rpcrypt->bright, rpcrypt->ambassador, nonce, morning); + chiaki_rpcrypt_bright_ambassador(target, rpcrypt->bright, rpcrypt->ambassador, nonce, morning); } CHIAKI_EXPORT void chiaki_rpcrypt_init_regist_ps4_pre10(ChiakiRPCrypt *rpcrypt, const uint8_t *ambassador, uint32_t pin) diff --git a/lib/src/session.c b/lib/src/session.c index 32cb646..b5b928f 100644 --- a/lib/src/session.c +++ b/lib/src/session.c @@ -609,7 +609,7 @@ static ChiakiErrorCode session_thread_request_session(ChiakiSession *session, Ch #ifdef _WIN32 CHIAKI_LOGE(session->log, "Failed to create socket to request session"); #else - CHIAKI_LOGE(session->log, "Failed to create socket to request session: %s", strerror(errno)); + CHIAKI_LOGE(session->log, "Failed to create socket to request session: %s", strerror(errno)); #endif free(sa); continue; From 42a3b864d07f7816085af5a79cf6f32c210abf3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Wed, 6 Jan 2021 22:04:53 +0100 Subject: [PATCH 161/237] Add _USE_MATH_DEFINES for Windows --- lib/src/feedback.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/src/feedback.c b/lib/src/feedback.c index 2ce0b98..1ae190e 100644 --- a/lib/src/feedback.c +++ b/lib/src/feedback.c @@ -1,5 +1,7 @@ // SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL +#define _USE_MATH_DEFINES + #include #include From 96cbd5d9b8d4ead2592ccd897cb99e3d3d89d0af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Sat, 9 Jan 2021 10:42:40 +0100 Subject: [PATCH 162/237] Fix Madgwick Filter for Orientation --- lib/include/chiaki/orientation.h | 4 +- lib/src/feedbacksender.c | 2 + lib/src/orientation.c | 68 +++++++++++++++++++++----------- 3 files changed, 50 insertions(+), 24 deletions(-) diff --git a/lib/include/chiaki/orientation.h b/lib/include/chiaki/orientation.h index 2322d55..6dde62b 100644 --- a/lib/include/chiaki/orientation.h +++ b/lib/include/chiaki/orientation.h @@ -22,7 +22,7 @@ typedef struct chiaki_orientation_t CHIAKI_EXPORT void chiaki_orientation_init(ChiakiOrientation *orient); CHIAKI_EXPORT void chiaki_orientation_update(ChiakiOrientation *orient, - float gx, float gy, float gz, float ax, float ay, float az, float time_step_sec); + float gx, float gy, float gz, float ax, float ay, float az, float beta, float time_step_sec); /** * Extension of ChiakiOrientation, also tracking an absolute timestamp and the current gyro/accel state @@ -33,7 +33,7 @@ typedef struct chiaki_orientation_tracker_t float accel_x, accel_y, accel_z; ChiakiOrientation orient; uint32_t timestamp; - bool first_sample; + uint64_t sample_index; } ChiakiOrientationTracker; CHIAKI_EXPORT void chiaki_orientation_tracker_init(ChiakiOrientationTracker *tracker); diff --git a/lib/src/feedbacksender.c b/lib/src/feedbacksender.c index 771796f..cafffff 100644 --- a/lib/src/feedbacksender.c +++ b/lib/src/feedbacksender.c @@ -116,10 +116,12 @@ static void feedback_sender_send_state(ChiakiFeedbackSender *feedback_sender) state.accel_x = feedback_sender->controller_state.accel_x; state.accel_y = feedback_sender->controller_state.accel_y; state.accel_z = feedback_sender->controller_state.accel_z; + state.orient_x = feedback_sender->controller_state.orient_x; state.orient_y = feedback_sender->controller_state.orient_y; state.orient_z = feedback_sender->controller_state.orient_z; state.orient_w = feedback_sender->controller_state.orient_w; + ChiakiErrorCode err = chiaki_takion_send_feedback_state(feedback_sender->takion, feedback_sender->state_seq_num++, &state); if(err != CHIAKI_ERR_SUCCESS) CHIAKI_LOGE(feedback_sender->log, "FeedbackSender failed to send Feedback State"); diff --git a/lib/src/orientation.c b/lib/src/orientation.c index bb0849e..7f07b09 100644 --- a/lib/src/orientation.c +++ b/lib/src/orientation.c @@ -1,18 +1,30 @@ // SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL #include +#include + +#define SIN_1_4_PI 0.7071067811865475 +#define SIN_NEG_1_4_PI -0.7071067811865475 +#define COS_1_4_PI 0.7071067811865476 +#define COS_NEG_1_4_PI 0.7071067811865476 + +#define WARMUP_SAMPLES_COUNT 30 +#define BETA_WARMUP 20.0f +#define BETA_DEFAULT 0.05f CHIAKI_EXPORT void chiaki_orientation_init(ChiakiOrientation *orient) { - orient->x = orient->y = orient->z = 0.0f; - orient->w = 1.0f; + // 90 deg rotation around x for Madgwick + orient->x = SIN_1_4_PI; + orient->y = 0.0f; + orient->z = 0.0f; + orient->w = COS_1_4_PI; } -#define BETA 0.1f // 2 * proportional gain static float inv_sqrt(float x); CHIAKI_EXPORT void chiaki_orientation_update(ChiakiOrientation *orient, - float gx, float gy, float gz, float ax, float ay, float az, float time_step_sec) + float gx, float gy, float gz, float ax, float ay, float az, float beta, float time_step_sec) { float q0 = orient->w, q1 = orient->x, q2 = orient->y, q3 = orient->z; // Madgwick's IMU algorithm. @@ -57,17 +69,22 @@ CHIAKI_EXPORT void chiaki_orientation_update(ChiakiOrientation *orient, s1 = _4q1 * q3q3 - _2q3 * ax + 4.0f * q0q0 * q1 - _2q0 * ay - _4q1 + _8q1 * q1q1 + _8q1 * q2q2 + _4q1 * az; s2 = 4.0f * q0q0 * q2 + _2q0 * ax + _4q2 * q3q3 - _2q3 * ay - _4q2 + _8q2 * q1q1 + _8q2 * q2q2 + _4q2 * az; s3 = 4.0f * q1q1 * q3 - _2q1 * ax + 4.0f * q2q2 * q3 - _2q2 * ay; - recip_norm = inv_sqrt(s0 * s0 + s1 * s1 + s2 * s2 + s3 * s3); // normalise step magnitude - s0 *= recip_norm; - s1 *= recip_norm; - s2 *= recip_norm; - s3 *= recip_norm; + recip_norm = s0 * s0 + s1 * s1 + s2 * s2 + s3 * s3; // normalise step magnitude + // avoid NaN when the orientation is already perfect or inverse to perfect + if(recip_norm > 0.000001f) + { + recip_norm = inv_sqrt(recip_norm); + s0 *= recip_norm; + s1 *= recip_norm; + s2 *= recip_norm; + s3 *= recip_norm; - // Apply feedback step - q_dot1 -= BETA * s0; - q_dot2 -= BETA * s1; - q_dot3 -= BETA * s2; - q_dot4 -= BETA * s3; + // Apply feedback step + q_dot1 -= beta * s0; + q_dot2 -= beta * s1; + q_dot3 -= beta * s2; + q_dot4 -= beta * s3; + } } // Integrate rate of change of quaternion to yield quaternion @@ -91,6 +108,9 @@ CHIAKI_EXPORT void chiaki_orientation_update(ChiakiOrientation *orient, static float inv_sqrt(float x) { +#if 1 + return 1.0f / sqrt(x); +#else // Fast inverse square-root // See: http://en.wikipedia.org/wiki/Fast_inverse_square_root float halfx = 0.5f * x; @@ -100,6 +120,7 @@ static float inv_sqrt(float x) y = *(float*)&i; y = y * (1.5f - (halfx * y * y)); return y; +#endif } CHIAKI_EXPORT void chiaki_orientation_tracker_init(ChiakiOrientationTracker *tracker) @@ -110,7 +131,7 @@ CHIAKI_EXPORT void chiaki_orientation_tracker_init(ChiakiOrientationTracker *tra tracker->gyro_x = tracker->gyro_y = tracker->gyro_z = 0.0f; chiaki_orientation_init(&tracker->orient); tracker->timestamp = 0; - tracker->first_sample = true; + tracker->sample_index = 0; } CHIAKI_EXPORT void chiaki_orientation_tracker_update(ChiakiOrientationTracker *tracker, @@ -122,9 +143,9 @@ CHIAKI_EXPORT void chiaki_orientation_tracker_update(ChiakiOrientationTracker *t tracker->accel_x = ax; tracker->accel_y = ay; tracker->accel_z = az; - if(tracker->first_sample) + tracker->sample_index++; + if(tracker->sample_index <= 1) { - tracker->first_sample = false; tracker->timestamp = timestamp_us; return; } @@ -133,7 +154,9 @@ CHIAKI_EXPORT void chiaki_orientation_tracker_update(ChiakiOrientationTracker *t delta_us += (1ULL << 32); delta_us -= tracker->timestamp; tracker->timestamp = timestamp_us; - chiaki_orientation_update(&tracker->orient, gx, gy, gz, ax, ay, az, (float)delta_us / 1000000.0f); + chiaki_orientation_update(&tracker->orient, gx, gy, gz, ax, ay, az, + tracker->sample_index < WARMUP_SAMPLES_COUNT ? BETA_WARMUP : BETA_DEFAULT, + (float)delta_us / 1000000.0f); } CHIAKI_EXPORT void chiaki_orientation_tracker_apply_to_controller_state(ChiakiOrientationTracker *tracker, @@ -145,8 +168,9 @@ CHIAKI_EXPORT void chiaki_orientation_tracker_apply_to_controller_state(ChiakiOr state->accel_x = tracker->accel_x; state->accel_y = tracker->accel_y; state->accel_z = tracker->accel_z; - state->orient_x = tracker->orient.x; - state->orient_y = tracker->orient.y; - state->orient_z = tracker->orient.z; - state->orient_w = tracker->orient.w; + // -90 deg rotation around x from Madgwick + state->orient_w = COS_NEG_1_4_PI * tracker->orient.w - SIN_NEG_1_4_PI * tracker->orient.x; + state->orient_x = COS_NEG_1_4_PI * tracker->orient.x + SIN_NEG_1_4_PI * tracker->orient.w; + state->orient_y = COS_NEG_1_4_PI * tracker->orient.y - SIN_NEG_1_4_PI * tracker->orient.z; + state->orient_z = COS_NEG_1_4_PI * tracker->orient.z + SIN_NEG_1_4_PI * tracker->orient.y; } From 7785c310a9bb4e37c56263504a4b5df99685e49f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Sun, 10 Jan 2021 11:04:19 +0100 Subject: [PATCH 163/237] Fix some Uninitialized Memory --- gui/src/streamsession.cpp | 3 ++- lib/src/frameprocessor.c | 2 ++ lib/src/senkusha.c | 2 +- lib/src/videoreceiver.c | 1 + 4 files changed, 6 insertions(+), 2 deletions(-) diff --git a/gui/src/streamsession.cpp b/gui/src/streamsession.cpp index 7d25136..3f46070 100644 --- a/gui/src/streamsession.cpp +++ b/gui/src/streamsession.cpp @@ -102,11 +102,12 @@ StreamSession::StreamSession(const StreamSessionConnectInfo &connect_info, QObje QByteArray host_str = connect_info.host.toUtf8(); - ChiakiConnectInfo chiaki_connect_info; + ChiakiConnectInfo chiaki_connect_info = {}; chiaki_connect_info.ps5 = chiaki_target_is_ps5(connect_info.target); chiaki_connect_info.host = host_str.constData(); chiaki_connect_info.video_profile = connect_info.video_profile; chiaki_connect_info.video_profile_auto_downgrade = true; + chiaki_connect_info.enable_keyboard = false; #if CHIAKI_LIB_ENABLE_PI_DECODER if(connect_info.decoder == Decoder::Pi && chiaki_connect_info.video_profile.codec != CHIAKI_CODEC_H264) diff --git a/lib/src/frameprocessor.c b/lib/src/frameprocessor.c index c072a98..bcee36b 100644 --- a/lib/src/frameprocessor.c +++ b/lib/src/frameprocessor.c @@ -48,6 +48,8 @@ CHIAKI_EXPORT void chiaki_frame_processor_init(ChiakiFrameProcessor *frame_proce frame_processor->buf_stride_per_unit = 0; frame_processor->units_source_expected = 0; frame_processor->units_fec_expected = 0; + frame_processor->units_source_received = 0; + frame_processor->units_fec_received = 0; frame_processor->unit_slots = NULL; frame_processor->unit_slots_size = 0; frame_processor->flushed = true; diff --git a/lib/src/senkusha.c b/lib/src/senkusha.c index ebab5d7..fc4e600 100644 --- a/lib/src/senkusha.c +++ b/lib/src/senkusha.c @@ -361,7 +361,7 @@ static ChiakiErrorCode senkusha_run_mtu_in_test(ChiakiSenkusha *senkusha, uint32 senkusha->state_failed = false; senkusha->mtu_id = ++request_id; - tkproto_SenkushaMtuCommand mtu_cmd; + tkproto_SenkushaMtuCommand mtu_cmd = { 0 }; mtu_cmd.id = request_id; mtu_cmd.mtu_req = cur; mtu_cmd.num = 1; diff --git a/lib/src/videoreceiver.c b/lib/src/videoreceiver.c index 52beb02..6468fcd 100644 --- a/lib/src/videoreceiver.c +++ b/lib/src/videoreceiver.c @@ -17,6 +17,7 @@ CHIAKI_EXPORT void chiaki_video_receiver_init(ChiakiVideoReceiver *video_receive video_receiver->frame_index_cur = -1; video_receiver->frame_index_prev = -1; + video_receiver->frame_index_prev_complete = 0; chiaki_frame_processor_init(&video_receiver->frame_processor, video_receiver->log); video_receiver->packet_stats = packet_stats; From a0c3768edb6ca6885b7a6d7b3cb8aa21469dd1db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Sun, 10 Jan 2021 14:46:24 +0100 Subject: [PATCH 164/237] Fix Idle Controller State on Android --- android/app/src/main/cpp/chiaki-jni.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/android/app/src/main/cpp/chiaki-jni.c b/android/app/src/main/cpp/chiaki-jni.c index 35543ad..49229f5 100644 --- a/android/app/src/main/cpp/chiaki-jni.c +++ b/android/app/src/main/cpp/chiaki-jni.c @@ -373,7 +373,8 @@ JNIEXPORT void JNICALL JNI_FCN(sessionSetSurface)(JNIEnv *env, jobject obj, jlon JNIEXPORT void JNICALL JNI_FCN(sessionSetControllerState)(JNIEnv *env, jobject obj, jlong ptr, jobject controller_state_java) { AndroidChiakiSession *session = (AndroidChiakiSession *)ptr; - ChiakiControllerState controller_state = { 0 }; + ChiakiControllerState controller_state; + chiaki_controller_state_set_idle(&controller_state); controller_state.buttons = (uint32_t)E->GetIntField(env, controller_state_java, session->java_controller_state_buttons); controller_state.l2_state = (uint8_t)E->GetByteField(env, controller_state_java, session->java_controller_state_l2_state); controller_state.r2_state = (uint8_t)E->GetByteField(env, controller_state_java, session->java_controller_state_r2_state); From 9ab84e60546d94be9ad726fb291fc92d9702c82b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Sun, 10 Jan 2021 16:01:37 +0100 Subject: [PATCH 165/237] Prefer fixed local Port for Discovery --- lib/include/chiaki/discovery.h | 2 ++ lib/src/discovery.c | 49 ++++++++++++++++++++++++---------- lib/src/discoveryservice.c | 4 +-- 3 files changed, 39 insertions(+), 16 deletions(-) diff --git a/lib/include/chiaki/discovery.h b/lib/include/chiaki/discovery.h index c9881b5..f26200b 100644 --- a/lib/include/chiaki/discovery.h +++ b/lib/include/chiaki/discovery.h @@ -25,6 +25,8 @@ extern "C" { #define CHIAKI_DISCOVERY_PROTOCOL_VERSION_PS4 "00020020" #define CHIAKI_DISCOVERY_PORT_PS5 9302 #define CHIAKI_DISCOVERY_PROTOCOL_VERSION_PS5 "00030010" +#define CHIAKI_DISCOVERY_PORT_LOCAL_MIN 9303 +#define CHIAKI_DISCOVERY_PORT_LOCAL_MAX 9319 typedef enum chiaki_discovery_cmd_t { diff --git a/lib/src/discovery.c b/lib/src/discovery.c index 0658c87..f207a54 100644 --- a/lib/src/discovery.c +++ b/lib/src/discovery.c @@ -152,27 +152,48 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_discovery_init(ChiakiDiscovery *discovery, return CHIAKI_ERR_NETWORK; } - memset(&discovery->local_addr, 0, sizeof(discovery->local_addr)); - discovery->local_addr.sa_family = family; - if(family == AF_INET6) + // First try CHIAKI_DISCOVERY_PORT_LOCAL_MIN..local_addr, 0, sizeof(discovery->local_addr)); + discovery->local_addr.sa_family = family; + if(family == AF_INET6) + { #ifndef __SWITCH__ - struct in6_addr anyaddr = IN6ADDR_ANY_INIT; + struct in6_addr anyaddr = IN6ADDR_ANY_INIT; #endif - struct sockaddr_in6 *addr = (struct sockaddr_in6 *)&discovery->local_addr; + struct sockaddr_in6 *addr = (struct sockaddr_in6 *)&discovery->local_addr; #ifndef __SWITCH__ - addr->sin6_addr = anyaddr; + addr->sin6_addr = anyaddr; #endif - addr->sin6_port = htons(0); - } - else // AF_INET - { - struct sockaddr_in *addr = (struct sockaddr_in *)&discovery->local_addr; - addr->sin_addr.s_addr = htonl(INADDR_ANY); - addr->sin_port = htons(0); + addr->sin6_port = htons(port); + } + else // AF_INET + { + struct sockaddr_in *addr = (struct sockaddr_in *)&discovery->local_addr; + addr->sin_addr.s_addr = htonl(INADDR_ANY); + addr->sin_port = htons(port); + } + + r = bind(discovery->socket, &discovery->local_addr, sizeof(discovery->local_addr)); + if(r >= 0 || !port) + break; + if(port == CHIAKI_DISCOVERY_PORT_LOCAL_MAX) + { + port = 0; + CHIAKI_LOGI(discovery->log, "Discovery failed to bind port %u, trying random", + (unsigned int)port); + } + else + { + port++; + CHIAKI_LOGI(discovery->log, "Discovery failed to bind port %u, trying one higher", + (unsigned int)port); + } } - int r = bind(discovery->socket, &discovery->local_addr, sizeof(discovery->local_addr)); if(r < 0) { CHIAKI_LOGE(discovery->log, "Discovery failed to bind"); diff --git a/lib/src/discoveryservice.c b/lib/src/discoveryservice.c index 09f31ef..f163092 100644 --- a/lib/src/discoveryservice.c +++ b/lib/src/discoveryservice.c @@ -238,7 +238,7 @@ static void discovery_service_host_received(ChiakiDiscoveryHost *host, void *use if(service->hosts_count == service->options.hosts_max) { CHIAKI_LOGE(service->log, "Discovery Service received new host, but no space available"); - goto r2con; + goto rzcon; } CHIAKI_LOGI(service->log, "Discovery Service detected new host with id %s", host->host_id); @@ -279,7 +279,7 @@ static void discovery_service_host_received(ChiakiDiscoveryHost *host, void *use if(change) discovery_service_report_state(service); -r2con: +rzcon: chiaki_mutex_unlock(&service->state_mutex); } From acf15480f2b69ad0e61835a0d2a0e73030b490ab Mon Sep 17 00:00:00 2001 From: h0neybadger Date: Mon, 11 Jan 2021 20:03:28 +0100 Subject: [PATCH 166/237] Add switch rumble and motion feedbacks --- switch/include/gui.h | 6 -- switch/include/host.h | 7 +- switch/include/io.h | 21 +++- switch/src/gui.cpp | 42 ++------ switch/src/host.cpp | 36 ++++++- switch/src/io.cpp | 235 ++++++++++++++++++++++++++++++++++-------- switch/src/main.cpp | 8 +- 7 files changed, 255 insertions(+), 100 deletions(-) diff --git a/switch/include/gui.h b/switch/include/gui.h index 38f13ec..033cf67 100644 --- a/switch/include/gui.h +++ b/switch/include/gui.h @@ -69,12 +69,6 @@ class PSRemotePlay : public brls::View // to send gamepad inputs Host *host; brls::Label *label; - ChiakiControllerState state = {0}; - // FPS calculation - // double base_time; - // int frame_counter = 0; - // int fps = 0; - public: PSRemotePlay(Host *host); ~PSRemotePlay(); diff --git a/switch/include/host.h b/switch/include/host.h index a93e7c9..1e8114d 100644 --- a/switch/include/host.h +++ b/switch/include/host.h @@ -54,7 +54,9 @@ class Host std::function chiaki_regist_event_type_finished_success = nullptr; std::function chiaki_event_connected_cb = nullptr; std::function chiaki_even_login_pin_request_cb = nullptr; + std::function chiaki_event_rumble_cb = nullptr; std::function chiaki_event_quit_cb = nullptr; + std::function io_read_controller_cb = nullptr; // internal state bool discovered = false; @@ -73,6 +75,7 @@ class Host std::string server_nickname; ChiakiTarget target = CHIAKI_TARGET_PS4_UNKNOWN; ChiakiDiscoveryHostState state = CHIAKI_DISCOVERY_HOST_STATE_UNKNOWN; + ChiakiControllerState controller_state = {0}; // mac address = 48 bits uint8_t server_mac[6] = {0}; @@ -96,7 +99,7 @@ class Host int FiniSession(); void StopSession(); void StartSession(); - void SendFeedbackState(ChiakiControllerState *); + void SendFeedbackState(); void RegistCB(ChiakiRegistEvent *); void ConnectionEventCB(ChiakiEvent *); bool GetVideoResolution(int *ret_width, int *ret_height); @@ -110,7 +113,9 @@ class Host void SetRegistEventTypeFinishedSuccess(std::function chiaki_regist_event_type_finished_success); void SetEventConnectedCallback(std::function chiaki_event_connected_cb); void SetEventLoginPinRequestCallback(std::function chiaki_even_login_pin_request_cb); + void SetEventRumbleCallback(std::function chiaki_event_rumble_cb); void SetEventQuitCallback(std::function chiaki_event_quit_cb); + void SetReadControllerCallback(std::function io_read_controller_cb); bool IsRegistered(); bool IsDiscovered(); bool IsReady(); diff --git a/switch/include/io.h b/switch/include/io.h index 85e2966..8466fb0 100644 --- a/switch/include/io.h +++ b/switch/include/io.h @@ -28,6 +28,12 @@ Omit khrplatform: False Reproducible: False */ +#ifdef __SWITCH__ +#include +#else +#include +#endif + #include extern "C" { @@ -65,6 +71,11 @@ class IO SDL_AudioDeviceID sdl_audio_device_id = 0; SDL_Event sdl_event; SDL_Joystick *sdl_joystick_ptr[SDL_JOYSTICK_COUNT] = {0}; +#ifdef __SWITCH__ + PadState pad; + HidSixAxisSensorHandle sixaxis_handles[4]; + HidVibrationDeviceHandle vibration_handles[2][2]; +#endif GLuint vao; GLuint vbo; GLuint tex[PLANES_COUNT]; @@ -86,7 +97,7 @@ class IO void SetOpenGlYUVPixels(AVFrame *frame); bool ReadGameKeys(SDL_Event *event, ChiakiControllerState *state); bool ReadGameTouchScreen(ChiakiControllerState *state); - + bool ReadGameSixAxis(ChiakiControllerState *state); public: // singleton configuration IO(const IO&) = delete; @@ -100,9 +111,11 @@ class IO void AudioCB(int16_t *buf, size_t samples_count); bool InitVideo(int video_width, int video_height, int screen_width, int screen_height); bool FreeVideo(); - bool InitJoystick(); - bool FreeJoystick(); - bool MainLoop(ChiakiControllerState *state); + bool InitController(); + bool FreeController(); + bool MainLoop(); + void UpdateControllerState(ChiakiControllerState *state); + void SetRumble(uint8_t left, uint8_t right); }; #endif //CHIAKI_IO_H diff --git a/switch/src/gui.cpp b/switch/src/gui.cpp index f31cba8..e77576c 100644 --- a/switch/src/gui.cpp +++ b/switch/src/gui.cpp @@ -42,6 +42,9 @@ HostInterface::HostInterface(Host *host) // when the host is connected this->host->SetEventConnectedCallback(std::bind(&HostInterface::Stream, this)); this->host->SetEventQuitCallback(std::bind(&HostInterface::CloseStream, this, std::placeholders::_1)); + // allow host to update controller state + this->host->SetEventRumbleCallback(std::bind(&IO::SetRumble, this->io, std::placeholders::_1, std::placeholders::_2)); + this->host->SetReadControllerCallback(std::bind(&IO::UpdateControllerState, this->io, std::placeholders::_1)); } HostInterface::~HostInterface() @@ -245,7 +248,7 @@ MainApplication::MainApplication(DiscoveryManager *discoverymanager) MainApplication::~MainApplication() { this->discoverymanager->SetService(false); - //this->io->FreeJoystick(); + this->io->FreeController(); this->io->FreeVideo(); } @@ -264,16 +267,15 @@ bool MainApplication::Load() // init chiaki gl after borealis // let borealis manage the main screen/window - if(!io->InitVideo(0, 0, SCREEN_W, SCREEN_H)) { brls::Logger::error("Failed to initiate Video"); } - brls::Logger::info("Load sdl joysticks"); - if(!io->InitJoystick()) + brls::Logger::info("Load sdl/hid controller"); + if(!io->InitController()) { - brls::Logger::error("Faled to initiate Joysticks"); + brls::Logger::error("Faled to initiate Controller"); } // Create a view @@ -541,38 +543,12 @@ PSRemotePlay::PSRemotePlay(Host *host) : host(host) { this->io = IO::GetInstance(); - - // store joycon/touchpad keys - for(int x = 0; x < CHIAKI_CONTROLLER_TOUCHES_MAX; x++) - // start touchpad as "untouched" - this->state.touches[x].id = -1; - - // this->base_time=glfwGetTime(); } void PSRemotePlay::draw(NVGcontext *vg, int x, int y, unsigned width, unsigned height, brls::Style *style, brls::FrameContext *ctx) { - this->io->MainLoop(&this->state); - this->host->SendFeedbackState(&this->state); - - // FPS calculation - // this->frame_counter += 1; - // double frame_time = glfwGetTime(); - // if((frame_time - base_time) >= 1.0) - // { - // base_time += 1; - // //printf("FPS: %d\n", this->frame_counter); - // this->fps = this->frame_counter; - // this->frame_counter = 0; - // } - // nvgBeginPath(vg); - // nvgFillColor(vg, nvgRGBA(255,192,0,255)); - // nvgFontFaceId(vg, ctx->fontStash->regular); - // nvgFontSize(vg, style->Label.smallFontSize); - // nvgTextAlign(vg, NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE); - // char fps_str[9] = {0}; - // sprintf(fps_str, "FPS: %000d", this->fps); - // nvgText(vg, 5,10, fps_str, NULL); + this->io->MainLoop(); + this->host->SendFeedbackState(); } PSRemotePlay::~PSRemotePlay() diff --git a/switch/src/host.cpp b/switch/src/host.cpp index 50fdd2f..573a887 100644 --- a/switch/src/host.cpp +++ b/switch/src/host.cpp @@ -156,9 +156,17 @@ int Host::InitSession(IO *user) // audio setting_cb and frame_cb chiaki_opus_decoder_set_cb(&this->opus_decoder, InitAudioCB, AudioCB, user); chiaki_opus_decoder_get_sink(&this->opus_decoder, &audio_sink); - chiaki_session_set_audio_sink(&(this->session), &audio_sink); - chiaki_session_set_video_sample_cb(&(this->session), VideoCB, user); - chiaki_session_set_event_cb(&(this->session), EventCB, this); + chiaki_session_set_audio_sink(&this->session, &audio_sink); + chiaki_session_set_video_sample_cb(&this->session, VideoCB, user); + chiaki_session_set_event_cb(&this->session, EventCB, this); + + // init controller states + chiaki_controller_state_set_idle(&this->controller_state); + + for(int x = 0; x < CHIAKI_CONTROLLER_TOUCHES_MAX; x++) + // start touchpad as "untouched" + this->controller_state.touches[x].id = -1; + return 0; } @@ -189,10 +197,13 @@ void Host::StartSession() } } -void Host::SendFeedbackState(ChiakiControllerState *state) +void Host::SendFeedbackState() { // send controller/joystick key - chiaki_session_set_controller_state(&this->session, state); + if(this->io_read_controller_cb != nullptr) + this->io_read_controller_cb(&this->controller_state); + + chiaki_session_set_controller_state(&this->session, &this->controller_state); } void Host::ConnectionEventCB(ChiakiEvent *event) @@ -209,6 +220,11 @@ void Host::ConnectionEventCB(ChiakiEvent *event) if(this->chiaki_even_login_pin_request_cb != nullptr) this->chiaki_even_login_pin_request_cb(event->login_pin_request.pin_incorrect); break; + case CHIAKI_EVENT_RUMBLE: + CHIAKI_LOGD(this->log, "EventCB CHIAKI_EVENT_RUMBLE"); + if(this->chiaki_event_rumble_cb != nullptr) + this->chiaki_event_rumble_cb(event->rumble.left, event->rumble.right); + break; case CHIAKI_EVENT_QUIT: CHIAKI_LOGI(this->log, "EventCB CHIAKI_EVENT_QUIT"); if(this->chiaki_event_quit_cb != nullptr) @@ -352,6 +368,16 @@ void Host::SetEventQuitCallback(std::function chiaki_ev this->chiaki_event_quit_cb = chiaki_event_quit_cb; } +void Host::SetEventRumbleCallback(std::function chiaki_event_rumble_cb) +{ + this->chiaki_event_rumble_cb = chiaki_event_rumble_cb; +} + +void Host::SetReadControllerCallback(std::function io_read_controller_cb) +{ + this->io_read_controller_cb = io_read_controller_cb; +} + bool Host::IsRegistered() { return this->registered; diff --git a/switch/src/io.cpp b/switch/src/io.cpp index e086215..65df801 100644 --- a/switch/src/io.cpp +++ b/switch/src/io.cpp @@ -1,11 +1,5 @@ // SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL -#ifdef __SWITCH__ -#include -#else -#include -#endif - #include "io.h" #include "settings.h" @@ -345,22 +339,23 @@ bool IO::FreeVideo() return ret; } -bool IO::ReadGameTouchScreen(ChiakiControllerState *state) +bool IO::ReadGameTouchScreen(ChiakiControllerState *chiaki_state) { #ifdef __SWITCH__ - hidScanInput(); - int touch_count = hidTouchCount(); + HidTouchScreenState sw_state = {0}; + bool ret = false; - if(!touch_count) + if(!hidGetTouchScreenStates(&sw_state, 1) || sw_state.count == 0) { + // flush state + chiaki_state->buttons &= ~CHIAKI_CONTROLLER_BUTTON_TOUCHPAD; // touchscreen release for(int i = 0; i < CHIAKI_CONTROLLER_TOUCHES_MAX; i++) { - if(state->touches[i].id != -1) + if(chiaki_state->touches[i].id != -1) { - state->touches[i].x = 0; - state->touches[i].y = 0; - state->touches[i].id = -1; - state->buttons &= ~CHIAKI_CONTROLLER_BUTTON_TOUCHPAD; // touchscreen release + chiaki_state->touches[i].x = 0; + chiaki_state->touches[i].y = 0; + chiaki_state->touches[i].id = -1; // the state changed ret = true; } @@ -368,32 +363,25 @@ bool IO::ReadGameTouchScreen(ChiakiControllerState *state) return ret; } - touchPosition touch; - for(int i = 0; i < touch_count && i < CHIAKI_CONTROLLER_TOUCHES_MAX; i++) + // scale switch screen to the PS trackpad + for(int i = 0; i < sw_state.count && i < CHIAKI_CONTROLLER_TOUCHES_MAX; i++) { - hidTouchRead(&touch, i); // 1280×720 px (16:9) // ps4 controller aspect ratio looks closer to 29:10 - uint16_t x = touch.px * (DS4_TRACKPAD_MAX_X / SWITCH_TOUCHSCREEN_MAX_X); - uint16_t y = touch.py * (DS4_TRACKPAD_MAX_Y / SWITCH_TOUCHSCREEN_MAX_Y); + uint16_t x = sw_state.touches[i].x * (DS4_TRACKPAD_MAX_X / SWITCH_TOUCHSCREEN_MAX_X); + uint16_t y = sw_state.touches[i].y * (DS4_TRACKPAD_MAX_Y / SWITCH_TOUCHSCREEN_MAX_Y); // use nintendo switch border's 5% to if(x <= (SWITCH_TOUCHSCREEN_MAX_X * 0.05) || x >= (SWITCH_TOUCHSCREEN_MAX_X * 0.95) || y <= (SWITCH_TOUCHSCREEN_MAX_Y * 0.05) || y >= (SWITCH_TOUCHSCREEN_MAX_Y * 0.95)) - { - state->buttons |= CHIAKI_CONTROLLER_BUTTON_TOUCHPAD; // touchscreen - // printf("CHIAKI_CONTROLLER_BUTTON_TOUCHPAD\n"); - } + chiaki_state->buttons |= CHIAKI_CONTROLLER_BUTTON_TOUCHPAD; // touchscreen else - { - state->buttons &= ~CHIAKI_CONTROLLER_BUTTON_TOUCHPAD; // touchscreen release - } + chiaki_state->buttons &= ~CHIAKI_CONTROLLER_BUTTON_TOUCHPAD; // touchscreen release - state->touches[i].x = x; - state->touches[i].y = y; - state->touches[i].id = i; - // printf("[point_id=%d] px=%03d, py=%03d, dx=%03d, dy=%03d, angle=%03d\n", - // i, touch.px, touch.py, touch.dx, touch.dy, touch.angle); + chiaki_state->touches[i].x = x; + chiaki_state->touches[i].y = y; + chiaki_state->touches[i].id = i; + // printf("[point_id=%d] x=%03d, y=%03d\n", i, x, y); ret = true; } return ret; @@ -402,14 +390,127 @@ bool IO::ReadGameTouchScreen(ChiakiControllerState *state) #endif } +void IO::SetRumble(uint8_t left, uint8_t right) +{ +#ifdef __SWITCH__ + Result rc = 0; + HidVibrationValue vibration_values[] = { + { + .amp_low = 0.0f, + .freq_low = 160.0f, + .amp_high = 0.0f, + .freq_high = 320.0f, + }, + { + .amp_low = 0.0f, + .freq_low = 160.0f, + .amp_high = 0.0f, + .freq_high = 320.0f, + }}; + + int target_device = padIsHandheld(&pad) ? 0 : 1; + if(left > 0) + { + // SDL_HapticRumblePlay(this->sdl_haptic_ptr[0], left / 100, 5000); + vibration_values[0].amp_low = (float)left / (float)100; + vibration_values[0].amp_high = (float)left / (float)100; + vibration_values[0].freq_low *= (float)left / (float)100; + vibration_values[0].freq_high *= (float)left / (float)100; + } + + if(right > 0) + { + // SDL_HapticRumblePlay(this->sdl_haptic_ptr[1], right / 100, 5000); + vibration_values[1].amp_low = (float)right / (float)100; + vibration_values[1].amp_high = (float)right / (float)100; + vibration_values[1].freq_low *= (float)left / (float)100; + vibration_values[1].freq_high *= (float)left / (float)100; + } + + // printf("left ptr %p amp_low %f amp_high %f freq_low %f freq_high %f\n", + // &vibration_values[0], + // vibration_values[0].amp_low, + // vibration_values[0].amp_high, + // vibration_values[0].freq_low, + // vibration_values[0].freq_high); + + // printf("right ptr %p amp_low %f amp_high %f freq_low %f freq_high %f\n", + // &vibration_values[1], + // vibration_values[1].amp_low, + // vibration_values[1].amp_high, + // vibration_values[1].freq_low, + // vibration_values[1].freq_high); + + rc = hidSendVibrationValues(this->vibration_handles[target_device], vibration_values, 2); + if(R_FAILED(rc)) + CHIAKI_LOGE(this->log, "hidSendVibrationValues() returned: 0x%x", rc); + +#endif +} + +bool IO::ReadGameSixAxis(ChiakiControllerState *state) +{ +#ifdef __SWITCH__ + // Read from the correct sixaxis handle depending on the current input style + HidSixAxisSensorState sixaxis = {0}; + uint64_t style_set = padGetStyleSet(&pad); + if(style_set & HidNpadStyleTag_NpadHandheld) + hidGetSixAxisSensorStates(this->sixaxis_handles[0], &sixaxis, 1); + else if(style_set & HidNpadStyleTag_NpadFullKey) + hidGetSixAxisSensorStates(this->sixaxis_handles[1], &sixaxis, 1); + else if(style_set & HidNpadStyleTag_NpadJoyDual) + { + // For JoyDual, read from either the Left or Right Joy-Con depending on which is/are connected + u64 attrib = padGetAttributes(&pad); + if(attrib & HidNpadAttribute_IsLeftConnected) + hidGetSixAxisSensorStates(this->sixaxis_handles[2], &sixaxis, 1); + else if(attrib & HidNpadAttribute_IsRightConnected) + hidGetSixAxisSensorStates(this->sixaxis_handles[3], &sixaxis, 1); + } + + // printf("Acceleration: x=% .4f, y=% .4f, z=% .4f\n", sixaxis.acceleration.x, sixaxis.acceleration.y, sixaxis.acceleration.z); + // printf("Angular velocity: x=% .4f, y=% .4f, z=% .4f\n", sixaxis.angular_velocity.x, sixaxis.angular_velocity.y, sixaxis.angular_velocity.z); + // printf("Angle: x=% .4f, y=% .4f, z=% .4f\n", sixaxis.angle.x, sixaxis.angle.y, sixaxis.angle.z); + // printf("Direction matrix:\n" + // " [ % .4f, % .4f, % .4f ]\n" + // " [ % .4f, % .4f, % .4f ]\n" + // " [ % .4f, % .4f, % .4f ]\n", + // sixaxis.direction.direction[0][0], sixaxis.direction.direction[1][0], sixaxis.direction.direction[2][0], + // sixaxis.direction.direction[0][1], sixaxis.direction.direction[1][1], sixaxis.direction.direction[2][1], + // sixaxis.direction.direction[0][2], sixaxis.direction.direction[1][2], sixaxis.direction.direction[2][2]); + + state->gyro_x = sixaxis.angular_velocity.x; + state->gyro_y = sixaxis.angular_velocity.y; + state->gyro_z = sixaxis.angular_velocity.z; + state->accel_x = sixaxis.acceleration.x; + state->accel_y = sixaxis.acceleration.y; + state->accel_z = sixaxis.acceleration.z; + + // thank you @thestr4ng3r for the hint + // https://en.wikipedia.org/wiki/Conversion_between_quaternions_and_Euler_angles#Euler_angles_to_quaternion_conversion + double cy = cos(sixaxis.angle.z * 0.5); + double sy = sin(sixaxis.angle.z * 0.5); + double cp = cos(sixaxis.angle.y * 0.5); + double sp = sin(sixaxis.angle.y * 0.5); + double cr = cos(sixaxis.angle.x * 0.5); + double sr = sin(sixaxis.angle.x * 0.5); + + state->orient_x = sr * cp * cy - cr * sp * sy; + state->orient_y = cr * sp * cy + sr * cp * sy; + state->orient_z = cr * cp * sy - sr * sp * cy; + state->orient_w = cr * cp * cy + sr * sp * sy; + return true; +#else + return false; +#endif +} + bool IO::ReadGameKeys(SDL_Event *event, ChiakiControllerState *state) { // return true if an event changed (gamepad input) // TODO // share vs PS button - // Gyro ? - // rumble ? bool ret = true; switch(event->type) { @@ -620,7 +721,7 @@ bool IO::InitOpenGlTextures() D(glGenTextures(PLANES_COUNT, this->tex)); D(glGenBuffers(PLANES_COUNT, this->pbo)); - uint8_t uv_default[] = { 0x7f, 0x7f }; + uint8_t uv_default[] = {0x7f, 0x7f}; for(int i = 0; i < PLANES_COUNT; i++) { D(glBindTexture(GL_TEXTURE_2D, this->tex[i])); @@ -633,7 +734,7 @@ bool IO::InitOpenGlTextures() D(glUseProgram(this->prog)); // bind only as many planes as we need - const char *plane_names[] = { "plane1", "plane2", "plane3" }; + const char *plane_names[] = {"plane1", "plane2", "plane3"}; for(int i = 0; i < PLANES_COUNT; i++) D(glUniform1i(glGetUniformLocation(this->prog, plane_names[i]), i)); @@ -786,7 +887,7 @@ inline void IO::OpenGlDraw() D(glFinish()); } -bool IO::InitJoystick() +bool IO::InitController() { // https://github.com/switchbrew/switch-examples/blob/master/graphics/sdl2/sdl2-simple/source/main.cpp#L57 // open CONTROLLER_PLAYER_1 and CONTROLLER_PLAYER_2 @@ -800,24 +901,64 @@ bool IO::InitJoystick() CHIAKI_LOGE(this->log, "SDL_JoystickOpen: %s\n", SDL_GetError()); return false; } + // this->sdl_haptic_ptr[i] = SDL_HapticOpenFromJoystick(sdl_joystick_ptr[i]); + // SDL_HapticRumbleInit(this->sdl_haptic_ptr[i]); + // if(sdl_haptic_ptr[i] == nullptr) + // { + // CHIAKI_LOGE(this->log, "SDL_HapticRumbleInit: %s\n", SDL_GetError()); + // } } +#ifdef __SWITCH__ +Result rc = 0; + // Configure our supported input layout: a single player with standard controller styles + padConfigureInput(1, HidNpadStyleSet_NpadStandard); + + // Initialize the default gamepad (which reads handheld mode inputs as well as the first connected controller) + padInitializeDefault(&this->pad); + // touchpad + hidInitializeTouchScreen(); + // It's necessary to initialize these separately as they all have different handle values + hidGetSixAxisSensorHandles(&this->sixaxis_handles[0], 1, HidNpadIdType_Handheld, HidNpadStyleTag_NpadHandheld); + hidGetSixAxisSensorHandles(&this->sixaxis_handles[1], 1, HidNpadIdType_No1, HidNpadStyleTag_NpadFullKey); + hidGetSixAxisSensorHandles(&this->sixaxis_handles[2], 2, HidNpadIdType_No1, HidNpadStyleTag_NpadJoyDual); + hidStartSixAxisSensor(this->sixaxis_handles[0]); + hidStartSixAxisSensor(this->sixaxis_handles[1]); + hidStartSixAxisSensor(this->sixaxis_handles[2]); + hidStartSixAxisSensor(this->sixaxis_handles[3]); + + rc = hidInitializeVibrationDevices(this->vibration_handles[0], 2, HidNpadIdType_Handheld, HidNpadStyleTag_NpadHandheld); + if(R_FAILED(rc)) + CHIAKI_LOGE(this->log, "hidInitializeVibrationDevices() HidNpadIdType_Handheld returned: 0x%x", rc); + + rc = hidInitializeVibrationDevices(this->vibration_handles[1], 2, HidNpadIdType_No1, HidNpadStyleTag_NpadJoyDual); + if(R_FAILED(rc)) + CHIAKI_LOGE(this->log, "hidInitializeVibrationDevices() HidNpadIdType_No1 returned: 0x%x", rc); + +#endif return true; } -bool IO::FreeJoystick() +bool IO::FreeController() { for(int i = 0; i < SDL_JOYSTICK_COUNT; i++) { - if(SDL_JoystickGetAttached(sdl_joystick_ptr[i])) - SDL_JoystickClose(sdl_joystick_ptr[i]); + SDL_JoystickClose(this->sdl_joystick_ptr[i]); + // SDL_HapticClose(this->sdl_haptic_ptr[i]); } +#ifdef __SWITCH__ + hidStopSixAxisSensor(this->sixaxis_handles[0]); + hidStopSixAxisSensor(this->sixaxis_handles[1]); + hidStopSixAxisSensor(this->sixaxis_handles[2]); + hidStopSixAxisSensor(this->sixaxis_handles[3]); +#endif return true; } -bool IO::MainLoop(ChiakiControllerState *state) +void IO::UpdateControllerState(ChiakiControllerState *state) { - D(glUseProgram(this->prog)); - +#ifdef __SWITCH__ + padUpdate(&this->pad); +#endif // handle SDL events while(SDL_PollEvent(&this->sdl_event)) { @@ -825,11 +966,17 @@ bool IO::MainLoop(ChiakiControllerState *state) switch(this->sdl_event.type) { case SDL_QUIT: - return false; + this->quit = true; } } ReadGameTouchScreen(state); + ReadGameSixAxis(state); +} + +bool IO::MainLoop() +{ + D(glUseProgram(this->prog)); glClearColor(0.0f, 0.0f, 0.0f, 1.0f); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); diff --git a/switch/src/main.cpp b/switch/src/main.cpp index 84f9210..34e4b20 100644 --- a/switch/src/main.cpp +++ b/switch/src/main.cpp @@ -83,12 +83,6 @@ extern "C" void userAppInit() // load socket custom config socketInitialize(&g_chiakiSocketInitConfig); setsysInitialize(); - - // padConfigureInput(1, HidNpadStyleSet_NpadStandard); - // PadState pad; - // padInitializeDefault(&pad); - - //hidInitializeTouchScreen(); } extern "C" void userAppExit() @@ -126,7 +120,7 @@ int main(int argc, char *argv[]) return 1; } - CHIAKI_LOGI(log, "Loading SDL audio / joystick"); + CHIAKI_LOGI(log, "Loading SDL audio / joystick / haptic"); if(SDL_Init(SDL_INIT_AUDIO | SDL_INIT_JOYSTICK)) { CHIAKI_LOGE(log, "SDL initialization failed: %s", SDL_GetError()); From 12054a91c9f98e88bd05986eadaeeddfacc106dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Mon, 11 Jan 2021 21:22:04 +0100 Subject: [PATCH 167/237] Fix Motion Data on Switch --- switch/src/io.cpp | 76 ++++++++++++++++++++++++++++------------------- 1 file changed, 46 insertions(+), 30 deletions(-) diff --git a/switch/src/io.cpp b/switch/src/io.cpp index 65df801..541c0de 100644 --- a/switch/src/io.cpp +++ b/switch/src/io.cpp @@ -468,37 +468,53 @@ bool IO::ReadGameSixAxis(ChiakiControllerState *state) hidGetSixAxisSensorStates(this->sixaxis_handles[3], &sixaxis, 1); } - // printf("Acceleration: x=% .4f, y=% .4f, z=% .4f\n", sixaxis.acceleration.x, sixaxis.acceleration.y, sixaxis.acceleration.z); - // printf("Angular velocity: x=% .4f, y=% .4f, z=% .4f\n", sixaxis.angular_velocity.x, sixaxis.angular_velocity.y, sixaxis.angular_velocity.z); - // printf("Angle: x=% .4f, y=% .4f, z=% .4f\n", sixaxis.angle.x, sixaxis.angle.y, sixaxis.angle.z); - // printf("Direction matrix:\n" - // " [ % .4f, % .4f, % .4f ]\n" - // " [ % .4f, % .4f, % .4f ]\n" - // " [ % .4f, % .4f, % .4f ]\n", - // sixaxis.direction.direction[0][0], sixaxis.direction.direction[1][0], sixaxis.direction.direction[2][0], - // sixaxis.direction.direction[0][1], sixaxis.direction.direction[1][1], sixaxis.direction.direction[2][1], - // sixaxis.direction.direction[0][2], sixaxis.direction.direction[1][2], sixaxis.direction.direction[2][2]); + state->gyro_x = sixaxis.angular_velocity.x * 2.0f * M_PI; + state->gyro_y = sixaxis.angular_velocity.z * 2.0f * M_PI; + state->gyro_z = -sixaxis.angular_velocity.y * 2.0f * M_PI; + state->accel_x = -sixaxis.acceleration.x; + state->accel_y = -sixaxis.acceleration.z; + state->accel_z = sixaxis.acceleration.y; - state->gyro_x = sixaxis.angular_velocity.x; - state->gyro_y = sixaxis.angular_velocity.y; - state->gyro_z = sixaxis.angular_velocity.z; - state->accel_x = sixaxis.acceleration.x; - state->accel_y = sixaxis.acceleration.y; - state->accel_z = sixaxis.acceleration.z; - - // thank you @thestr4ng3r for the hint - // https://en.wikipedia.org/wiki/Conversion_between_quaternions_and_Euler_angles#Euler_angles_to_quaternion_conversion - double cy = cos(sixaxis.angle.z * 0.5); - double sy = sin(sixaxis.angle.z * 0.5); - double cp = cos(sixaxis.angle.y * 0.5); - double sp = sin(sixaxis.angle.y * 0.5); - double cr = cos(sixaxis.angle.x * 0.5); - double sr = sin(sixaxis.angle.x * 0.5); - - state->orient_x = sr * cp * cy - cr * sp * sy; - state->orient_y = cr * sp * cy + sr * cp * sy; - state->orient_z = cr * cp * sy - sr * sp * cy; - state->orient_w = cr * cp * cy + sr * sp * sy; + // https://d3cw3dd2w32x2b.cloudfront.net/wp-content/uploads/2015/01/matrix-to-quat.pdf + float (*dm)[3] = sixaxis.direction.direction; + float m[3][3] = { + { dm[0][0], dm[2][0], dm[1][0] }, + { dm[0][2], dm[2][2], dm[1][2] }, + { dm[0][1], dm[2][1], dm[1][1] } + }; + std::array q; + float t; + if(m[2][2] < 0) + { + if (m[0][0] > m[1][1]) + { + t = 1 + m[0][0] - m[1][1] - m[2][2]; + q = { t, m[0][1] + m[1][0], m[2][0] + m[0][2], m[1][2] - m[2][1] }; + } + else + { + t = 1 - m[0][0] + m[1][1] -m[2][2]; + q = { m[0][1] + m[1][0], t, m[1][2] + m[2][1], m[2][0] - m[0][2] }; + } + } + else + { + if(m[0][0] < -m[1][1]) + { + t = 1 - m[0][0] - m[1][1] + m[2][2]; + q = { m[2][0] + m[0][2], m[1][2] + m[2][1], t, m[0][1] - m[1][0] }; + } + else + { + t = 1 + m[0][0] + m[1][1] + m[2][2]; + q = { m[1][2] - m[2][1], m[2][0] - m[0][2], m[0][1] - m[1][0], t }; + } + } + float fac = 0.5f / sqrt(t); + state->orient_x = q[0] * fac; + state->orient_y = q[1] * fac; + state->orient_z = -q[2] * fac; + state->orient_w = q[3] * fac; return true; #else return false; From 1b8fa556f88399fdd72c0db5e97f18891741d398 Mon Sep 17 00:00:00 2001 From: h0neybadger Date: Mon, 11 Jan 2021 22:10:51 +0100 Subject: [PATCH 168/237] Fix switch touchpad resolution --- switch/src/io.cpp | 55 ++++++++++++++++++++--------------------------- 1 file changed, 23 insertions(+), 32 deletions(-) diff --git a/switch/src/io.cpp b/switch/src/io.cpp index 541c0de..2b4dab5 100644 --- a/switch/src/io.cpp +++ b/switch/src/io.cpp @@ -345,43 +345,34 @@ bool IO::ReadGameTouchScreen(ChiakiControllerState *chiaki_state) HidTouchScreenState sw_state = {0}; bool ret = false; - if(!hidGetTouchScreenStates(&sw_state, 1) || sw_state.count == 0) - { - // flush state - chiaki_state->buttons &= ~CHIAKI_CONTROLLER_BUTTON_TOUCHPAD; // touchscreen release - for(int i = 0; i < CHIAKI_CONTROLLER_TOUCHES_MAX; i++) - { - if(chiaki_state->touches[i].id != -1) - { - chiaki_state->touches[i].x = 0; - chiaki_state->touches[i].y = 0; - chiaki_state->touches[i].id = -1; - // the state changed - ret = true; - } - } - return ret; - } - + hidGetTouchScreenStates(&sw_state, 1); // scale switch screen to the PS trackpad - for(int i = 0; i < sw_state.count && i < CHIAKI_CONTROLLER_TOUCHES_MAX; i++) + chiaki_state->buttons &= ~CHIAKI_CONTROLLER_BUTTON_TOUCHPAD; // touchscreen release + for(int i = 0; i < CHIAKI_CONTROLLER_TOUCHES_MAX; i++) { + if((i + 1) <= sw_state.count) + { + uint16_t x = sw_state.touches[i].x * ((float)DS4_TRACKPAD_MAX_X / (float)SWITCH_TOUCHSCREEN_MAX_X); + uint16_t y = sw_state.touches[i].y * ((float)DS4_TRACKPAD_MAX_Y / (float)SWITCH_TOUCHSCREEN_MAX_Y); - // 1280×720 px (16:9) - // ps4 controller aspect ratio looks closer to 29:10 - uint16_t x = sw_state.touches[i].x * (DS4_TRACKPAD_MAX_X / SWITCH_TOUCHSCREEN_MAX_X); - uint16_t y = sw_state.touches[i].y * (DS4_TRACKPAD_MAX_Y / SWITCH_TOUCHSCREEN_MAX_Y); + // use nintendo switch border's 5% to trigger the touchpad button + if(x <= (DS4_TRACKPAD_MAX_X * 0.05) || x >= (DS4_TRACKPAD_MAX_X * 0.95) || y <= (DS4_TRACKPAD_MAX_Y * 0.05) || y >= (DS4_TRACKPAD_MAX_Y * 0.95)) + chiaki_state->buttons |= CHIAKI_CONTROLLER_BUTTON_TOUCHPAD; // touchscreen - // use nintendo switch border's 5% to - if(x <= (SWITCH_TOUCHSCREEN_MAX_X * 0.05) || x >= (SWITCH_TOUCHSCREEN_MAX_X * 0.95) || y <= (SWITCH_TOUCHSCREEN_MAX_Y * 0.05) || y >= (SWITCH_TOUCHSCREEN_MAX_Y * 0.95)) - chiaki_state->buttons |= CHIAKI_CONTROLLER_BUTTON_TOUCHPAD; // touchscreen + chiaki_state->touches[i].x = x; + chiaki_state->touches[i].y = y; + chiaki_state->touches[i].id = i; + } else - chiaki_state->buttons &= ~CHIAKI_CONTROLLER_BUTTON_TOUCHPAD; // touchscreen release - - chiaki_state->touches[i].x = x; - chiaki_state->touches[i].y = y; - chiaki_state->touches[i].id = i; - // printf("[point_id=%d] x=%03d, y=%03d\n", i, x, y); + { + // flush touch state + chiaki_state->touches[i].x = 0; + chiaki_state->touches[i].y = 0; + chiaki_state->touches[i].id = -1; + } + // printf("switch id=%d x=%03d, y=%03d\nchiaki id=%d x=%03d, y=%03d\n", + // i, sw_state.touches[i].x, sw_state.touches[i].y, + // chiaki_state->touches[i].id, chiaki_state->touches[i].x, chiaki_state->touches[i].y); ret = true; } return ret; From fc58e83e9cd75c0eb8b4d7afd9d910c0f13945b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Tue, 12 Jan 2021 12:23:02 +0100 Subject: [PATCH 169/237] Track Finger IDs on Switch --- switch/include/host.h | 5 ++-- switch/include/io.h | 6 +++-- switch/src/gui.cpp | 2 +- switch/src/host.cpp | 8 ++---- switch/src/io.cpp | 59 ++++++++++++++++++++++++++----------------- 5 files changed, 46 insertions(+), 34 deletions(-) diff --git a/switch/include/host.h b/switch/include/host.h index 1e8114d..e28183e 100644 --- a/switch/include/host.h +++ b/switch/include/host.h @@ -56,7 +56,7 @@ class Host std::function chiaki_even_login_pin_request_cb = nullptr; std::function chiaki_event_rumble_cb = nullptr; std::function chiaki_event_quit_cb = nullptr; - std::function io_read_controller_cb = nullptr; + std::function *)> io_read_controller_cb = nullptr; // internal state bool discovered = false; @@ -76,6 +76,7 @@ class Host ChiakiTarget target = CHIAKI_TARGET_PS4_UNKNOWN; ChiakiDiscoveryHostState state = CHIAKI_DISCOVERY_HOST_STATE_UNKNOWN; ChiakiControllerState controller_state = {0}; + std::map finger_id_touch_id; // mac address = 48 bits uint8_t server_mac[6] = {0}; @@ -115,7 +116,7 @@ class Host void SetEventLoginPinRequestCallback(std::function chiaki_even_login_pin_request_cb); void SetEventRumbleCallback(std::function chiaki_event_rumble_cb); void SetEventQuitCallback(std::function chiaki_event_quit_cb); - void SetReadControllerCallback(std::function io_read_controller_cb); + void SetReadControllerCallback(std::function *)> io_read_controller_cb); bool IsRegistered(); bool IsDiscovered(); bool IsReady(); diff --git a/switch/include/io.h b/switch/include/io.h index 8466fb0..3c1031d 100644 --- a/switch/include/io.h +++ b/switch/include/io.h @@ -35,6 +35,8 @@ Reproducible: False #endif #include +#include + extern "C" { #include @@ -96,7 +98,7 @@ class IO GLuint CreateAndCompileShader(GLenum type, const char *source); void SetOpenGlYUVPixels(AVFrame *frame); bool ReadGameKeys(SDL_Event *event, ChiakiControllerState *state); - bool ReadGameTouchScreen(ChiakiControllerState *state); + bool ReadGameTouchScreen(ChiakiControllerState *state, std::map *finger_id_touch_id); bool ReadGameSixAxis(ChiakiControllerState *state); public: // singleton configuration @@ -114,7 +116,7 @@ class IO bool InitController(); bool FreeController(); bool MainLoop(); - void UpdateControllerState(ChiakiControllerState *state); + void UpdateControllerState(ChiakiControllerState *state, std::map *finger_id_touch_id); void SetRumble(uint8_t left, uint8_t right); }; diff --git a/switch/src/gui.cpp b/switch/src/gui.cpp index e77576c..2d0b71f 100644 --- a/switch/src/gui.cpp +++ b/switch/src/gui.cpp @@ -44,7 +44,7 @@ HostInterface::HostInterface(Host *host) this->host->SetEventQuitCallback(std::bind(&HostInterface::CloseStream, this, std::placeholders::_1)); // allow host to update controller state this->host->SetEventRumbleCallback(std::bind(&IO::SetRumble, this->io, std::placeholders::_1, std::placeholders::_2)); - this->host->SetReadControllerCallback(std::bind(&IO::UpdateControllerState, this->io, std::placeholders::_1)); + this->host->SetReadControllerCallback(std::bind(&IO::UpdateControllerState, this->io, std::placeholders::_1, std::placeholders::_2)); } HostInterface::~HostInterface() diff --git a/switch/src/host.cpp b/switch/src/host.cpp index 573a887..2f50e2f 100644 --- a/switch/src/host.cpp +++ b/switch/src/host.cpp @@ -163,10 +163,6 @@ int Host::InitSession(IO *user) // init controller states chiaki_controller_state_set_idle(&this->controller_state); - for(int x = 0; x < CHIAKI_CONTROLLER_TOUCHES_MAX; x++) - // start touchpad as "untouched" - this->controller_state.touches[x].id = -1; - return 0; } @@ -201,7 +197,7 @@ void Host::SendFeedbackState() { // send controller/joystick key if(this->io_read_controller_cb != nullptr) - this->io_read_controller_cb(&this->controller_state); + this->io_read_controller_cb(&this->controller_state, &finger_id_touch_id); chiaki_session_set_controller_state(&this->session, &this->controller_state); } @@ -373,7 +369,7 @@ void Host::SetEventRumbleCallback(std::function chiaki_e this->chiaki_event_rumble_cb = chiaki_event_rumble_cb; } -void Host::SetReadControllerCallback(std::function io_read_controller_cb) +void Host::SetReadControllerCallback(std::function *)> io_read_controller_cb) { this->io_read_controller_cb = io_read_controller_cb; } diff --git a/switch/src/io.cpp b/switch/src/io.cpp index 2b4dab5..409bba0 100644 --- a/switch/src/io.cpp +++ b/switch/src/io.cpp @@ -339,7 +339,7 @@ bool IO::FreeVideo() return ret; } -bool IO::ReadGameTouchScreen(ChiakiControllerState *chiaki_state) +bool IO::ReadGameTouchScreen(ChiakiControllerState *chiaki_state, std::map *finger_id_touch_id) { #ifdef __SWITCH__ HidTouchScreenState sw_state = {0}; @@ -348,31 +348,44 @@ bool IO::ReadGameTouchScreen(ChiakiControllerState *chiaki_state) hidGetTouchScreenStates(&sw_state, 1); // scale switch screen to the PS trackpad chiaki_state->buttons &= ~CHIAKI_CONTROLLER_BUTTON_TOUCHPAD; // touchscreen release - for(int i = 0; i < CHIAKI_CONTROLLER_TOUCHES_MAX; i++) + + // un-touch all old touches + for(auto it = finger_id_touch_id->begin(); it != finger_id_touch_id->end();) { - if((i + 1) <= sw_state.count) + auto cur = it; + it++; + for(int i = 0; i < sw_state.count; i++) { - uint16_t x = sw_state.touches[i].x * ((float)DS4_TRACKPAD_MAX_X / (float)SWITCH_TOUCHSCREEN_MAX_X); - uint16_t y = sw_state.touches[i].y * ((float)DS4_TRACKPAD_MAX_Y / (float)SWITCH_TOUCHSCREEN_MAX_Y); - - // use nintendo switch border's 5% to trigger the touchpad button - if(x <= (DS4_TRACKPAD_MAX_X * 0.05) || x >= (DS4_TRACKPAD_MAX_X * 0.95) || y <= (DS4_TRACKPAD_MAX_Y * 0.05) || y >= (DS4_TRACKPAD_MAX_Y * 0.95)) - chiaki_state->buttons |= CHIAKI_CONTROLLER_BUTTON_TOUCHPAD; // touchscreen - - chiaki_state->touches[i].x = x; - chiaki_state->touches[i].y = y; - chiaki_state->touches[i].id = i; + if(sw_state.touches[i].finger_id == cur->first) + goto cont; } - else + if(cur->second >= 0) + chiaki_controller_state_stop_touch(chiaki_state, (uint8_t)cur->second); + finger_id_touch_id->erase(cur); +cont: + continue; + } + + + // touch or update all current touches + for(int i = 0; i < sw_state.count; i++) + { + uint16_t x = sw_state.touches[i].x * ((float)DS4_TRACKPAD_MAX_X / (float)SWITCH_TOUCHSCREEN_MAX_X); + uint16_t y = sw_state.touches[i].y * ((float)DS4_TRACKPAD_MAX_Y / (float)SWITCH_TOUCHSCREEN_MAX_Y); + // use nintendo switch border's 5% to trigger the touchpad button + if(x <= (DS4_TRACKPAD_MAX_X * 0.05) || x >= (DS4_TRACKPAD_MAX_X * 0.95) || y <= (DS4_TRACKPAD_MAX_Y * 0.05) || y >= (DS4_TRACKPAD_MAX_Y * 0.95)) + chiaki_state->buttons |= CHIAKI_CONTROLLER_BUTTON_TOUCHPAD; // touchscreen + + auto it = finger_id_touch_id->find(sw_state.touches[i].finger_id); + if(it == finger_id_touch_id->end()) { - // flush touch state - chiaki_state->touches[i].x = 0; - chiaki_state->touches[i].y = 0; - chiaki_state->touches[i].id = -1; + // new touch + (*finger_id_touch_id)[sw_state.touches[i].finger_id] = + chiaki_controller_state_start_touch(chiaki_state, x, y); } - // printf("switch id=%d x=%03d, y=%03d\nchiaki id=%d x=%03d, y=%03d\n", - // i, sw_state.touches[i].x, sw_state.touches[i].y, - // chiaki_state->touches[i].id, chiaki_state->touches[i].x, chiaki_state->touches[i].y); + else if(it->second >= 0) + chiaki_controller_state_set_touch_pos(chiaki_state, (uint8_t)it->second, x, y); + // it->second < 0 ==> touch ignored because there were already too many multi-touches ret = true; } return ret; @@ -961,7 +974,7 @@ bool IO::FreeController() return true; } -void IO::UpdateControllerState(ChiakiControllerState *state) +void IO::UpdateControllerState(ChiakiControllerState *state, std::map *finger_id_touch_id) { #ifdef __SWITCH__ padUpdate(&this->pad); @@ -977,7 +990,7 @@ void IO::UpdateControllerState(ChiakiControllerState *state) } } - ReadGameTouchScreen(state); + ReadGameTouchScreen(state, finger_id_touch_id); ReadGameSixAxis(state); } From 0b6e479e0bafb2b1fdeb0832eb886cb78a8388b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Tue, 12 Jan 2021 12:59:04 +0100 Subject: [PATCH 170/237] Fix some Double Free and uninitialized Memory in Switch --- switch/include/discoverymanager.h | 2 +- switch/src/discoverymanager.cpp | 6 ------ switch/src/main.cpp | 7 +++++-- 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/switch/include/discoverymanager.h b/switch/include/discoverymanager.h index 76c504d..49c8dcc 100644 --- a/switch/include/discoverymanager.h +++ b/switch/include/discoverymanager.h @@ -24,7 +24,7 @@ class DiscoveryManager struct sockaddr * host_addr = nullptr; size_t host_addr_len = 0; uint32_t GetIPv4BroadcastAddr(); - bool service_enable; + bool service_enable = false; public: typedef enum hoststate diff --git a/switch/src/discoverymanager.cpp b/switch/src/discoverymanager.cpp index e9abe5f..7fa7122 100644 --- a/switch/src/discoverymanager.cpp +++ b/switch/src/discoverymanager.cpp @@ -35,19 +35,13 @@ DiscoveryManager::~DiscoveryManager() { // join discovery thread if(this->service_enable) - { SetService(false); - } - - chiaki_discovery_fini(&this->discovery); } void DiscoveryManager::SetService(bool enable) { if(this->service_enable == enable) - { return; - } this->service_enable = enable; diff --git a/switch/src/main.cpp b/switch/src/main.cpp index 34e4b20..6b7c95e 100644 --- a/switch/src/main.cpp +++ b/switch/src/main.cpp @@ -129,8 +129,11 @@ int main(int argc, char *argv[]) // build sdl OpenGl and AV decoders graphical interface DiscoveryManager discoverymanager = DiscoveryManager(); - MainApplication app = MainApplication(&discoverymanager); - app.Load(); + { + // scope to delete MainApplication before SDL_Quit() + MainApplication app(&discoverymanager); + app.Load(); + } CHIAKI_LOGI(log, "Quit applet"); SDL_Quit(); From bb4e5398b289a1b6d6a301fe2278ffbe74e895e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Tue, 12 Jan 2021 13:35:40 +0100 Subject: [PATCH 171/237] Update Switch Metadata and Icons --- switch/CMakeLists.txt | 8 ++++---- switch/nro_icon.jpg | Bin 0 -> 20589 bytes switch/nro_icon.png | Bin 0 -> 14350 bytes switch/res/icon.jpg | Bin 25090 -> 0 bytes switch/res/icon.png | Bin 0 -> 7791 bytes switch/src/gui.cpp | 2 +- 6 files changed, 5 insertions(+), 5 deletions(-) create mode 100644 switch/nro_icon.jpg create mode 100644 switch/nro_icon.png delete mode 100644 switch/res/icon.jpg create mode 100644 switch/res/icon.png diff --git a/switch/CMakeLists.txt b/switch/CMakeLists.txt index 89bd789..4916a2e 100644 --- a/switch/CMakeLists.txt +++ b/switch/CMakeLists.txt @@ -126,9 +126,9 @@ install(TARGETS chiaki-borealis if(CHIAKI_IS_SWITCH) add_nro_target(chiaki chiaki-borealis - "chiaki" - "Chiaki team" + "Chiaki" + "H0neyBadger and thestr4ng3r" "${CHIAKI_VERSION}" - "${CMAKE_SOURCE_DIR}/switch/res/icon.jpg" - "${CMAKE_SOURCE_DIR}/switch/res") + "${CMAKE_CURRENT_SOURCE_DIR}/nro_icon.jpg" + "${CMAKE_CURRENT_SOURCE_DIR}/res") endif() diff --git a/switch/nro_icon.jpg b/switch/nro_icon.jpg new file mode 100644 index 0000000000000000000000000000000000000000..0ed61ec2d18a7af214198cddada4fbb1bdb5df4b GIT binary patch literal 20589 zcmeIabzD^4)&P9y?hvJhkdp51kWK*sfdK{>y1S)AS`?H<0YL!)Nof$opb+BVKt$o&xv-g@gA3vV~2vro76#*n9WPlj_0Ozx$ zWePrUYXDGIW&AAwgXcwzmPD=DjML3lZNxH)-wK~8R79#L){Q663h51%L>wcyhb|y1LPMnQQW2t&euI%gjS$-4g5@Ybv_l8)(SL)RduTW7z z7&)0C^y~~&R4hWQ?A$#3{QR`cA`-&9;+%Z^ya*vkSXfxNIJgvecoe*JRCK)mb~N!<$ByRxoQDTezLOho_gfkFVeD@QBE$=$P1))U@=w z8TamI<`)zeJ$hVRQd;%Ax~BF;U427)M`u@e&#T_P!J*-i(XsIl6SH&k3yVw3E1y@l zcfRiK?SDHsJVMxou=8j7ZrR`2MFiS~jEahaih-~T3E3NAI1wru9S=IOj5Y?;m4u!* z6q8goIj^z}i-AvPi_F4p0GphVfA;D&!n6y^{x!qG{#Ta$GVBk#CIDO%B=F&(5CKxa zo)Jra3nd>mOMb|I<|=K7EP@poWo511Tc4xtT#r8{(7l1F;-($jXl|6>^!nyOvm*|_ zkwmJ75;FYl$jPOW(`&IU2XS-uJHd12j_=O_yoT_e`LVrc`R!Xzp|{-CS-2$57K6Jx z>k9+X6asoUUp&#!%qsaL8Qd&JyQZdYpcB2wwE^-y=Ob7%&73K{J6d(zGLyPa@Ob6f zgD)qJbE?GR4^0Gmk$PUeUk6XtciDL_JxQU24UPjGQejJcO6duTqC7U zJAY=WtMqKZaDq0rI+(C03#SI|dN<iE zPKG~)ry80*S}COVFZrTWSGI|JAf>j|RY$&k_(DEtXR)XMQ^$LMgAuF4ZXB-mdVjIe zld)$CQ^6`%$v-TM60|$Y?&&)kxx$E+`^+-s_P$6NZF+1*WnURev(z-spf;V^d^bC^ zTe!`fSRIUiH{!vRDZwEW>$qi;=Jw&SLFQ@;X_HJ)r%{s^j~5Z=(uv_Ypk`pgkiB$v z4hS*Qo;I}L88(k+kKkuP9kL)*wK0LjS5bCJ_n+4!tJE~H6KH8&IjjvPv=?GmnLB~+3F5*L* zuO|bB+Ki$#RDJ5L$$hJX`c3t1$li(G z{!wh-R?BUVa{wnWuRKnbzdzU?LYXecat?@(8up(9Scz>a6s2a(#>Ook-%M@oI3)1z z#IiiQ!_JZ)3;Zh=TVxH)wb=HXU~PsoTs&GsxVSHFNGj;OqtRW>gS!GOVh&|QuX^uVSIbd;>cChGf`&%nkjzVJA)p82@f)P&& zkB6K~XESpw44i?d7;`>PgIgt8oQYQk1=`GNW$11KyUc9CoKo031xjQ(;g#M_JaD0) zb6{*Ps^|;nlda8;%zQ!ih{DH(cYCa*@N*G#&J3a3T93}-vIUIx$yPAz(&Ar`Ov(a1q(rj#|{b;;ifz*HnjZ|CupI>^H zUL<|q=lf(LI1ZKF2n=~lQ7=HT(fXAuF7u3dS~lX)LESY`bC$OM?8Yk}470PgZE*sI zxzXisd{$dXE5Rs<9RhJ>cJB6~&7*lDhs#N|QVn7Rt>!nbocd#=4HaduQ;n{f8MI6d zy>EMMoO5Hqqw#2FLRX zAU~D`ycU~+pPd8d2Nu=|`%@-oOH`hydr~3qJ{!-veC-@fpm1h3cj`$wI5s=`v@O0) zxlvPl+Al0lD4Nr`yw&A-4q)z`6m>`N?bK&ZOXhS+C7-PIvF#_8yeY8TZwSX&4wD`~ z+uD#KN;WIAGVO`&IiRz84!j%rCiRuTHc-G6zU^#wED?T%k?Z(%;-@!v zUJ;;dOZ7E%{4QcdEbd8Mz1j=DUjA^)fOx|*>&Bh8q&{kD_9K}ocWPgl=D0lAC{qC4 z?E|5g6yDxB@Wq)#3HMo?{YTlu70Lt6<(_%dJu^0B8B{8wPu0OM_HvGJ%z|g+&Vhu0 zTF;K039=DCcWtldLqT2LUP?oH2K_CQNAFJ2p0#Yzodey8zc-Vq)KP}PSGL_8Fy=QkccE$?94AU&=eM2)|8DH4D`Sdsuf*DTuaEn&WaBd(JB?8z z$B#M>N5a4numrMC5T1DsOt3Q#Fa>`yKHI1IChJQZdgl4rbLUO-NLOLLF?=CXn&+h; zTQHA-)ZduuWYeB_DJI=dS6gRvbZu;tL!+$aDVTVh>rQ6xk?<%7aAL4Cm^^fb@Hh5j1RD5t33z~DXOQBjuOuSSyFtq zo$H^@fe4#9=fnLdaQ^1d9}d8H1~&>Ad!Z#?A!ZLDkD;BNubL| zFnlP<7>uV%-&K0ItF6G-g;*hqI*PUW?LT?k;ct(sj@rSZmP^ccC*9_ZEGMRAd4l%% zq-IqT{TxWXrg{!MRRSv)ct0pn>Kn)^qdct_(BtlWwP?6@FoZFtPZ&cOGBbRm034ZK zb(I8<4d)yYm95$&T!r(4D=XxGX*oLZUTVQ&D0ppE>J(Go({_b+hX>xWw?}&x&cc?{ zomhA+d;8ro5S%qwP*w;~ zf?uTiIjUm9X@vFRZdlVaN-Q2T(t zD^0r5`+jA%^5;Z9aE%<%!gY!?1Uf*@8BR?2aZwbdV-&=otp4o`rR6~TG!!vBGJ(S*()7tSa8&-fxi|RiD3#nf;Dc%TgTSn1w$cdODw=c=HoE5U5PHyRPJlYM{Sk{5EthSw*yB=Jv2| z;uXDyrNZ*Ha{!RIztct5G6%l!yqEw=uTwCwT%`12tU^+>Yq4n zU2aB>z2@IeLz&SzvEUU#^GlxC9W-(SfpTOvuYJacil8J4-<_&%5Tb;dmJHNFEtB!p19yf{#nWL zz0|H$L|_iK^{2jIozl|}0g{?8-hJJpcU|?3*H#wqCB1H{WL{dzt1N*Qh%?iI+cWzR z4`Pn@vCcn=dU9w*)PQK~S$nos&Q_AC(hl7OKKwKTjysTlX{h^;Z8H3)HY@&TIa22z&S&wHY&|?&M7g+}+&Q6E z&K59EOJ_$eAE*l#4<|PlATH(O0=2YWorr?Z<47mtXD z2p2am7cVadNWtOm>*N9T;c#+i_$fgi=5FZ*ckzHbJ3$Zm-8~e% z{;20qsonK_U0_@~Fn4E9H%pj;7tG0n;b&zo4xaA6*X8LBLlj-;>R@Hf1*&=R#D3Bv z_JV)WBO1&G?&xwsjiCHVWo7w`#>Lak;R0`E$pv$OIf9kDgA#du(Sm%xi2qc5gix?W zMCF|=JrV56@)GojT1Bm#E#X$87oZTI2%iWqOqhemQc#$KpW6b)0p+n0;Naom6XxOO z6&8Y83jLI$?BwnNb+UvZ2-Ed|BASRtkcXRJfL~BR zK!{&Rfd8i$eVCg&=!6JS7aR2S5GzYjMNkkF^a{8m)CR_7=K{02P+nBd4F>gacGGiq zc95X|wXKiv=^qX&s^)A7ML6{ZCCutaf02dSaDgD#UsL@(M%UKa+v$Jc{z>zbSH{i5 z+u6-t(@oRD4rb}`7u^3y^^;!*97^0h+j-W}=%v!ee|XlV;|vVno473js^$>4wDR3bcrmf(Sc5QhLC=!*Q@f;=3; z7Em4zK5jt^3t>U1ps*nSPkwi2YY%Uz8%)Lq^lAs z`3pBU{lzmuj6UD>Ms!xNpZsAGq9=|0{k^MNJ9Rb z{@VioZGr!`z<*odzb)|J7Wn^P3;gkR33CD$Y2M(wGqOzI-7?A=- zljJ&BI=Z8B1AwEGhntR)ECdXWfM6_wu^PDG2M1t-TDrT)Xlbea4AJ<~5ffAUuvJ zxA+c!LBKW_Ffs@Nq;7gTa-eO9;2wz0Z?MI0u%)e=BS^y!(lA;%I)U<$4Zp)y2si)% zJ34rRx?LoMg}5-M8@gc52wqfx5}*vI16lwCumC&(IN$(y032ZM1eUl1Iv`&5FZi!q z@N0ow79bZKumm~e0cXGwfL`zeh-U!Ofa#C6b+_gNBags@gewaGXrIo{zg`6+pi%+g zH0k{OIPd)YG#`v6`Un86PJi$_=K_H69$23AM;T)d01$)&KyBwAWfmC#@FEHTC_cDA z-Jlospn%`V)?l>N&SL<;y9od!?*RbY_RJ2UlB}T@XpT&CTh^}kuPF>5N&ahFtN}vu+fl_P{BYTYD5piKz#j5 zVEt(5m{=$n=Re|U2vG>ZM_QvnJ=yw;=pO)#87b*sg*+yARK8BBV478X7#XZ)OZ95w zWrX>?R(C6{bY!gCaK7vRla$EJf)!5i#Or28nSlF{=GwQ-9&i)7Hbpe?GmTaYoMkP} zyL{9j{~M*qdsRd~LnK@hp08sfJ=uIgcWb;$C$GF*(_9{v^9pz}(dser($;Ruy8MVB zE$+bc`geK&Da_i`gJYbMI@8v%`V5v{& z%f^-NgYj~F6M31XQ?oD{@_#~h8I|pej#-&G zjum#!;tbXr^Dy7u+lHk>NvSK+KPvp02{KU%Bj8{>U93JvzfGqiM8sn9u%Jojx${Bi?aBzPnbD5{WF_TO5G8+lF0W9mZiDR2B$RB36o8oJ0iPtsdkU#b&%;$t`DR ze-6zT#f>8-P}&>U%GWnxCHs4q{GP-!tQL7TH-D*-{FJ|W!!_S*?Go!D%`EBkm4)}z zVPoBO{TK`W5$xx{O=Zp1ZRDD0vNGv%3p9JeYlfjlo!Fxg{mS19H58-ozyIREFTWG{ zcbI=94|1uSWrh9g`rKU&rEBlYMw_w89vak2@5)4pyf~UZ z$*e`6K3Q#AF3WuN+^Vq=bR98oSpIswn2vsMws%$l6gx+AzR%MSfSn&u+a zcvXTiX`NCwKViEk>)mCBP6sZw6#xFomRBS1U|B-NpBT-3W?iwo<;mDX-s`ft~QcS%|A2XD6VxDA86{{+RB{SkHRM)%wU?J+I5@E zV5r+$yIRjF=d#?f_@*{7rI9KBX>ak9Dm%6>nX!L=6T>;-mti%0RqE3Tf zeXiwGeZJk46EmDFye-p&w!4AsJEskGLW8PX@tjmbZGQUQH+QSv?f;;?5oi#l|N7?L zTSAv$tNmc~0q=08N)1y=xpLtX9XQ)Rz2RwcWGH3uok0E<>1&u$7o+hnlWAi`Y9x9~ zh%>=OC0+poN%L$_5~U13$n9ilVmIteBwSzb?UDDey*DQ#xR+1eTR0_Jg*{a4<&@-u zH)z3bb{(9uz$GCTIxY$Z>cy<^BUT$+?-5_3yiT}&6?@=^-vOtsF>s*JS0X7D_ExtIhGhdS6izN6*VdiEwloulSKvi6eDxb&v&1xU88IvIP86Yu3iytB=Jo zuRbp1iy>z$fcd4GJ=C1NL-CkvKG7~pk&TSUFc>}_E%b8cmXtb4AaphQm>D_C=6c&s zG=pJ*{h`9Pw$&Kv4OV$^kyZ~RoMF>EEob08zyS9%NP&kpaX^Y0-^9xJ_S2&cOx}>Q z;XdxB)HC5LJPcHz8@%QkEsXn#R9;sa!^Tmw6&q#PUY$aF6#ihTfpxBg09q*{_OfX; z@SV+2{1mBfduRR~G9lklt?81o_zvz`ZkDN?E}AVZFdVm0b>yf*m6X(4(k6x5_uJ<)xTk_) z^8J@GW-KPLa4*Ap?eLfKl5@*-I7nOl#X8yxhv6zgD=cKb@FPy2ZVdC^#M_iQA}I&`$eFXb=rYr|C*5 zo(3JgbWThM#=#nJBy-AmYIzb!>_3`XAzL^&0TYKYbW(ZVx5QEo>Kr1Dn8anSmVEe% zJFQLFfvnV=rVk5aInIXIA$zy(PM-Q5njI{HRbzDuKJAj+$gQNXT0MWqaBadP)i zafqr^VlmkTvUQVJ)BaLJJ2HyP;S4f^i!lb93+debYTK0zNzZrBKA{Wy zG19K5%rs7PZ`M-sPm75@#AkN*777lZKYYZngCU<`&V!XNI(RFBXWZ*1H82GIsCbQzPDZnI7z znJeeEfKVT`y5oF!w|U<(jyFEbSF59Up0i&<>zQxr(~9*aE?&=d=hWrl+E=P_nUFIv zpCG@N%Y$VqzHuuym8`=P_kIUlakJtx{?>{Q=}<6>;;rH0*nlDX0q0<&NBAzYJA|%5 z%rqM&tnilgJ-(Z3^rmODexJX^03vfijJvh>LYHTrtAmkR@Jue#l;v%l4BaM7y8hHH z=Ay4uzAmGW&NSnzOSBda9}CgXZ4{|cedt++SeznFAD2DUJyjg?Rtozd`uHf}(MBR# zm8a$&rTP>Nj@4?;s$f=wIC<}g)UczFhYv?;X>jp`)5?}Hv)(zNT|G50Q>#&ORse4- zXmj`}=l!?|Fa9t^J7qGd>~^PG5uG!&WQ;V56iWyNk9z(Gu_S?iL-+#Tg>J2YRu|_T z!!*vzm+jas)?ZaIu$S1^Ne5g z5T~FZO9`^)n>Y6A7B$s`2*#5)_%CHwR^}gY*0M~J27J`6Wf|u~n_j=K@c3x^(N-dL zRh;JDy!tGS7>qn;tvjn-+^dg6YAiu0+-E(_DEQeGXUy$Mmg;lBl>Zaae62S8tO?%4 z(BqIlbm}wYqjcxPzai&C+B#F`6j8h=`-dBet4VQ>ED2h3p+sLz-wrzP42*YLyh$<4 zuEcMEaR;a{A>sVrEA%IZ9j}Dp8WqY=ZB*D@ri`gHSogH| zO`Vz;c8JZ9<{F9lR3XOQnj1`{+1E`a%e%dT92+br_2h&GH4TrE*XhY)<);<%uXWAE zV&5U>ey~K%#1pwqb6>i2C{tbRF`-F2_sE8Nuf*%GEJ*82l1bi?JbqsWhHT#G`t{Fv zq_I#u!kw|W$AyHnLHPW!`n?+MwT@6?wR}qocO(_9CV8bg8YJYR;8u} z5A}ez{nRONdMT2lo^P&l)e@3Kg#eVd>!+b~`yGpPWy?d|*gH%f&sfs{O|+I=U3{nT^=tee}vV z%F8AtZr`-!!aIg9e<=vK8zUZioBO(Qbglh_-fb_siK~@Ob+!u$OBP8C++5IubD*?i z_OhICvwJivOa0nWx!$8kL^Np&`f9=J=35TMsUmW3nza~pPe@S=V_^JZHm|lWe>6N` zNZDJrCSq+No~WpTKYT^Lll)d<6Ov(P*)d@hWya!UJLx=BS{U{qDDz(5tHf9{QZnb(Rm0 zR2FeO3vZPn<1S8K^}Y4VU|Ip+`2qe}w#EkeP3oE_`PSS)d-WZ$c^LP)D#&6Xt7?k{ z^-+ujIL3uIE0-3c0v!tEN_mG+&jDO>bM8VZ@yDbO@4p$byeptsfhX_HXcf)5l*6h! zwj1ie(REYx!)6Ts;_)kr}i7t z>^I2Pqsh56n#y_R{Zd-C_CEFJH?J?tzr6pPiZ2pItR;+*+x2{p;|wt7!|NeqwRiPP~jW0upFySc9)mOD zUCU*Uz8%ROho?-QqY<#kv!=}jc<473%L9Ot@ZJ|?bqN7VB>}$F<=pJhdWh#eTdnoy z5wsN9UzEf}HLshD&_|NwyWq#_<#<5ZdKA6&bDrj6@9ehuV4ux~Bz8bc3 zk3sR*3Ut$L!kcdr#WqR3yJO;s9tC+I)P*v9)xWcmC^fz(=#IyYjnOQ)55CM^EE8AY zy*^&a@j;mq>fE4|2W(Uccdu9L2B#7RGJCkEBUAcAj+U`c$4vY1s94>ESXi$t71XJ# z98YQX3;C1MQhOx~w_Hx$OK_C=$Wj!z^>_)VIVSS;$7HV}gLtCA%RcV+QL+Lceoe0_BB62aO5#{{y0Z@%RPT11#zM#+v?FC@_Q5{jLfb z83GYN!fhuXgrD3B{O1y0)`ZK;%PZzHJW#Tt>S=WAY9&>{sWy2t3eXTXnd~DZbk|g8 z248Y5cVx=x`e<*g{CL~@%IH5wyx6hS#|y!oT?-SCekAaoXPBF`{#bFkV|)%CY%G?M zY&9ULraMVRE%dq6Eke3UYkk{DwTIWXfVp=);cdb0ph#H0Da*d`fj^Zx*P4wQRR~!J zGlpHgihZl@BPi?h{V3+%mS;x(ZVoW1_i_`D=N5Y2D@~MCrsbG6?k_JK3hk|FI;qD$ zKEm|ZJ2AO;gn4{JkT-R0Ea>A&4%1mPe7(0a4g9edfn82qDy(~;rgwfA1Drl>E=<2VxKEsH+jB4^w zd0dH}<#uQ?X~eC81mhRz4+IDFT(!@p3aug}3ax0_c6Ct9P-}-`HpWU@7Bl&z!i7g_ zLS?+4%|PX>n^Xh4#sqS;&?fyoyu-f(Pk^^O6~s=@vKAGYMq{QWP-_Q zmJE5Dj4mt^>3Q@3O16k!L$rspWOO2sSg66WZ;%6Pf=e$X%HAoOy3|jP(u(q#HkP_( z>A9CWF+H9Uz5e=u8fP*+DT@}q;d9=!h`TTYmkqqk)sV|WM7w%3G_y-pZyqptmc3v*rHb!m4=YPe-Q{U+`9 zjh!Z636BP1y+NeKrhsXl8BV9cuhSfjf_cPBw&gEec#sun>Yf^x%mfco6tw zLm!C<0RJ068%ikc8VX4^&(qp+soeg#moG(k!l{1TzjEEZbM0+pd8vonqG;e?ZI_DI ziqb5tSKKQTJW~pTZ?yNc)6?R3ijua;7)j~KOK?JLrD`73OK&iyoA>6SC~Dp3#Ga3s z97sM2f1VkpxHY4DtuH}cx+s$QF3L>qW5a8^D)FqT#|)~85fNh3fp5qqq_(^y9;_C3 z9pZS&5>H??=@EKVskDc{;pqOpg(2a-Z!giR#pQq-91^5uR!gd|7F1xfNhG9fr@rwMqnp&=J?ovVkuX%!Oe2lr* zmJfe?t6>VIGBF;DNZs9TyjE@&`d#>Y0S*W)Ry>blCrmH|lh;?yjNHb6Diczn;Q?@P>2Jn@RhvXvZa))ulMu^@&|x77j=(bfMZZ ziTPS}=dJsLn>$x9yIye(D+>iaO}RRp!%4VQm@G}yQq{}u%wm-??|N89JkD92iYM#2 zZ%FsB2u^>?jz*LrtD5`^`D5}m!Yd^zyeAzGYw3nU8tpV6x;3mz`l`NIJQ!nm^ff>J z1-FgkJw4+jBdSTpcH2Z7%OS4S6`xf@JmcQFtv3Tk0qgi&GvmJWtL1*6Spb4%~L+kgTJteR^%g1*_1$`cv)c{YDK)fw4_STYgK8<1ZFr3sKl=-BcU-X zXS}qO^mQ6)CpL83RrcYFJxVh#rDbqqgsrg>m%ti-D3i|dvBNwJlQ{fo3uS#|8)Fiq zGPPJWLe1|4s{GZl$;R}9XGy#q6sdtM(yJ}Fs3;nY({_ZVtZ*{=T3{1>pD-ide9kNF za3GG49TL+e?BT{ymIf?6AHObcNx(OK9gl6cAgtNVPVa6Mw{DwBy@1$Z22o$On*i~g zi1rD_VF%b1F8;>h;`AO|i~nUpG5>`^i}!W;c|-5B^5cI;z<(YIXZ!rK96+y&{skiw zN`H?I085a;z!5Me;38pyXT~f)Dy0HTTo9*>wUdsyNb4^`&_Lij*`I&gg&>*y#L!72 zgYgUB(_g`CU|_;enx8pXMeSq=iZS?oQ5RAOA|&BQlCb&-fjh$Hf50d+f587?2pq!L ziNIqWaLXA33|aUt9SI=X{yv65z(K5Si`zVTK&#RvZ}wssL!9am9&^6CtQkt>xaR@x zT)ZTClzhuzB-G&?U^mbmGuz-$eDH>XizI`VaIv^{@oQmSjc`!3@bF@zNcvjYhoSdz zD~WBGgS$gVcZ8^NK0Za2v~9a>U*&&xB8D`OX&<24)SxNTw$8;-poY2V&Yebe-U&P-o#wRK8Of<* zV;6#HYw#C<6cWTI)O@R}hc4^SG*H&3Tbz(KCmoJ5v&~dl`U`mD`}lrlJ>>>Zn;RW} zD+)X|t)a-_ovK2JaqEkqKD?CnTti#R%_%Qc>==0Q`U@Dw3L4&B22J8O zXWuFSa&`bM4nTrpv>55fW~kq~F{JzWBrjZX^CM;Hr;l$xV-F$TfTYCr@z!~nGZ)Znm_e@rCq6Xr-++F4&CiJs;WXyS zd0Sd80~Zd1IFukfVIl9LOYM&@o<^>`6L@`fXqd1NMT@=p?(2GMbf)z9NIB znPwjIBs^*@t(l179x^rWS@l!~YfjnwiZ5|dt~dhq$`%d}@HiD?RH2=bu?h|OpNz?P zl-vlLj?^+$Q%rNuNFcIqc~+|BX!F*Y*sqANBGrnaVX|*;c3)X<4XsIuhpnLR5VbEj z1{O=s?_Nzjy=w7#Fly)N>g#f$ZWn@`(I~CfkM+A;FN(2l5b3mCM)7CwqX=udtACOf z^%j+gA$rd61xKIk<0LE|&B_k>7)-kB>PS`CEb@%^*^()cv)HmiXyltzIDjGf_(@GG ze8AP9qSa*siS=bgJqn#qJRkaMUCThOEZRD=DYs(8L3|sbN|eHFf@_hy@n~9XxwdQ- zc27-HH!Wg_xT_NeSag`_Dtf&FQHS!$v_j=oqce!wEz)oLr&YP(P$gaZY+ZxC&k{<~ zmoO}T)8!bw7UoLSto^*-s@k!r68G%@AnP51Wt5Q33@LcpTc)Bz^|Gye+(gZBPTls) zWWfJB(FN3~#`(hq>-UC#|K)g;``glk%# zGHqdn6TgeBs*p7lOGwgu@=Wc1Z!Ps)s0WF}(c`xYTkHIL<^uInd2hmdTNO?rlnQf=w@_1 zC~mub!P<#tN7>S1i-Y;Fm;$ZX>Fl%+OFW>NcVEnH-w(RIv-^5{{q={>Q9ZARxGZ0A z**v19Qr6IUr0j2DXrGpcahUW&3?lRE!t`MigczcQ`kPkD(uCnfD-uF!*+CtCD2aYm z=M0&(E()JgHxRX`JIRX0vA3duVMpZ_oJr^I_1UUmX!ZBBBF$WC$VB;3k%NsFq9e zujMD<39FoKU}c!Cl-gnB<@bhKb<#&Hm2^Dan2nF@H)6$(l>6?l!D`zpDVMI;)|!=N z?8@$nvudzQUP6BOe!=Y(=WQ}NcSB#Lwj^AO+epqCGQP#8a?WDo7UH+Gwro}NF=L}m;M~p&9o;4W0emSQInH5Qn*nsPd?D@ z9^c6x0x;ruI1{FWQIuOBN{N+I+_P;cluyD*>wT6h806Epi<$(nAxo7$a^rWupgKG{ zD3nxZF-Zdy`9jGeh#@NKP5Vz6g}D+}h^a8CD9{4bi>(Z8re<}?UI@$Fp%_@L6p%ZH zL_hG*(IlOuXymyOzA#7%mzU{^hG0trJDkOZ0t%%E%pFu}C{sIScN5X*dm0X`=nScA zkY_Bru-?culaIbgYOZ0{a;Z&4*#opg{o+UoI9Y)P*=V21%LDl$q)(Ix# zg7@B~>&A^6={HS)E9{GRxZg*j-laHj4RoR;9SLcdq`22vwkl#?>=dq1`p8&n{fadJ zG4~!M0?+YnjkJYkFV))mr~gz}Z# z$Vk}8BsIKNy|I|ZgWPpNt?CwecBN13_AvMr4pcFb(AiJ)f3REBvp>*877T%X&Lu+W zbjyfChMJ0L6N)Qg`BX3=EkBBIyD6M455rVGY~X6qra#^z1~WR%w@9J*iBMc6CFuAw z@ma=I-&E}h>e#Q{#EJ#?g{Z0XH2XsH@ZUjEd%p4^l>A;9_lp>z+p>UsVqkfbOj8yi z@>Rc`rC(THbA#nx|3Q)CZoP16 c+d@_R)p&ZyY~ENN?0~nns`Sa}?D>cP2V7a#P5=M^ literal 0 HcmV?d00001 diff --git a/switch/nro_icon.png b/switch/nro_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..65d745e017c63890d92698a06a862cccf62a7f5b GIT binary patch literal 14350 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 literal 0 HcmV?d00001 diff --git a/switch/res/icon.jpg b/switch/res/icon.jpg deleted file mode 100644 index cb515da077efce82a1d9aab5241a7cf15d30fd57..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 25090 zcmcG$1yo&KlOTG~0KwheA-KD{ySux)26qV_+}$BKBsf8WI|O&vpzr4Ur>Ez=)jiX_ zX1&dVQ@iZemQ%NC3-3$szX4uh*2T%YQfXvv`)k#=MNe=i=%l!fX(gXmrpZ@Cl zPuc!s0)m;jt0@405rfD=rcN$yAlL{5b9lNr{e?3?FuJLgu^9-i2Ep_$AOk`0(qFvE zzu?oqu;o86I0yvLUDQ-WL2W|^!NivT3vBYgz@}C%_8=ZM5D&STy#q);xYoa5v%j$4 zU)bKx9c0@-xtfbl=_z~3h03d||K4;9CR$ zU@tovyBPoTIS`<~;1(7Da9suf2-*OE_6q>ubpO?FAlhF#P_zgDYM{Q7n*e~!EC8Ui z1nJiMPudLw68N9}_P^!%AN}{f3W)xlzyDX|uaf^O1%HL#e*h>jV4!&p0fqvAqkuu6 zfV~d@gdhYMB*fnr>EFr+Xc$;H2uLXKzjOi+Q>ZufPN+D9|1&cd{cd%1YGNy+;q?Y0*DWm(z@wBGHcxUvw!~KK%c~G zg7fah3_j`lu2&E(?EtzMRu<19$v^+398`iT90 zdIVG{Wm?bpFjk(2LQ9r=<_JbO_<|pGB>1b1kCim`^o5?ziCD4CLqe_V;VB5j?IKtF z4jgxNq%&=R=D(5iH$~F?KiDt8v2>XOyFHaX>3V$S=w6ILahAu98rUFhT26~{?W)Qi z=!Ih;-R^eHncz$WV0vXq2c8JbfDbzU>>Uq^b>=&hL^VDuo=gB#sslcssE^>ahxlbv z{k@SI0NK*>P<%F#0^ln>u4=~p^5HiE!27?MAfoUKJ$@sLB0pu{=G=U31p-`4yEfMS z@a%s&H@WY<11r6ssImcw-jzkVQ@=N9bwD>jFv`@&8Ptk_vcf=8xKT`KW53&HmJP-Z zAj-wRn(qncD)c0taj$N&w-x{lCHDM3#$GfA;of^PbjO?zUV|k6RiIUT4i=Xf%RG>w zOS!SiSH~ao|BVE`{pMHG=)CCwQEeTQ;-i=|6$x@GW_d~_gBNj^`Og|k{#o9raY7*e zJ1&>C11wJmCl$Vm(n*yg5DJ3ng?0UHmimO*RgsU}_`LXZ%F*c*uB{UZ|B@I)Un|0V z@M($@rx?q!GiOKyVOWv+`8Us4;+wG8ZhlA*9xIIBKOpv1#nfL-{dL(0W6X}8Ra|rK z6U>bX(l^{!Al$eh{6k>#7|*!g>=Z1vZv`*w>KZS2Ekpk>AIOn_9Xai_FEE_12O-G2 zP$_>iEC6|XaIv!aDzpNbyA6sDv`zZF7?dFdi=aUT^!Q+PEp}?xfW_IiyHi~c!M`^B ztFS~Y2K4;k=d2IL73dgaiM>N$Nl1`Ns&6 ztGTrfpV*BjAB5Fy{V}|lw~Q40Xve%0xY<_Y^H(?TdVx#Nh~i$R5)kV)MlV}{DFng} zES7FE?pHVcNdmcK07&B;yDjKY2?Csc>^SsD2wz?O{Z{i1vV%~h_Jb(-OO9PNL+_L!bk_^w- z{u)C~pq~K$XBID>K-5{{~vm`e^mX|2J)M9$e_sx0RayEKV2(0$fZI9U>{(xn2?Fl zFqzpogiz2iP?bPl6y~p!g#v#Ea(q|jchGFEqPlhNdzZ2MwA2JiY2y>C39x^+t+a*G z-PVWVrERxWQi@cg=lnrw1`FzYCCLE)+$d_Re5|O#K#HE5A+^iV4fNp>x}S%VG`3V? z#!Vj;Bu3C*CEyM4ON;l?uw%dooMsZDkYK@ykD(wy6nv~1JH9fTJ3Nl6A>UN8O@)OZ zpYiFgQea6HhR&SPqI3IIO_FHbe(S|dU&RuE137xNysQU?97I_bL$mAU{%Y&9AQVQP zGyEGHXk@sfj_6b3w^2%roDiQBqkNT?ibgl&`cX`lW%5}4JKHu?V|aH>AZ7C8nPbb% z(2N{L1iM3)Z*XtFt!u{b57BH0ixWnebAgasAJ?lBr5A%DE~6r?MKt3O3JkcN0mgu5 z22%=DHu^6(XT*4Q$?`wTb8Mx%1(IDzgVU34znEAG5+u<4tmB2##Jf$=OKCL+q zQc-$RaTwElPrhIl^KHuR{Mmp_p1tH({3TjR(aqcP3CU^*wXgV!MzB*rwl@l^T9x72 zuYd8|Ok;EBBpO_w%f;E?b8;^E7(fTlpUL3&9nxmA+Ew>1-Zr3N!f}DS=4qI{S8M;< z3I2j)+J-g#5%i>UagzVPV_!|PvJbe%+Ir=jKXox^{|{ae_Wz}(+Lq5ar`f}%<{fxK zt7|F2y7=|g!#v%#dpEBoa=kYy`6=5xibkhMlWxl)euNP2nr$Hz1<(gafj5xInvL}k zn$(2Du*la@PGjpy*!Pmt)A4*v*`O72bSkIHg*NbY3U`R<11*iypru}#63MGYa`W4gX=zZDCA z;m1zalAf#X$9P*~=LtIo?wYV+_J1dh{3=1VCX#c7Q|3-O{r?jGBe{lZsLwF-OH50~ zYC>PAw@2$;ESZ}|xM_BgU5c(!urt_PqnN=}ya3eit0aWBr^#v&QXH zK=b7M)54CsWn(3>7??j0DvGAc|HYNjj>Jj5Kq=C16op!c4Z)1$8&@9$cq1R)^ihHw z11TC|vDD5&3#Jwv(hN3Wq{dXp5uZQ@g}v4W(t>weFGiQP*0w^)SM8P)hN1=l^{+82 zi*=A+7eC8aF0}L>yaOO#qJ#p9AR!@OKYoCPhJyO*Wxznr1cezOaYjWKR#r7GD4c;p zMk6L=Vi8dZP7-oSZumZZ`GJky#LfLnQ!_LMCYkzYQKi(zS=Jw0+gJZM9FSWQ1bYV< z$YB#dv2AJZp&XTMDq!R)6K46}LgFCX38+ZPn+4l{be@-1?@B7hu<7Ujs1X69m<9c{ zss_1OI#W3wyihWjDIbs$nYaeah-;2QrNu=35Na$mpZ^2n07-pfGUFE~fmzA{e!;0m zHR~_r3S_%$mi#0PY@|5u^3s7@TI})X&~Vsc9&t(&TdoOuhrd16x$nOHRwe+_uTC75KZk^*o# zC8eKCxV6CH|LyDuv)E*q3wVw-)i8p{)X2W$u?`8D!a zGOQt=%BQD01!{f`#cRZ;MYH(W4dt8U{-`2YlJpG2=4*0fkh#pC>coB|%d%LkY|5Y` zG|fXvN|nIUTh+)B%i~J8kA0kk$5_bXV@6m`8JVC$nfgkA&E@{}37CH!@QDbZ{Zxes z`#D;PiCnRr>>WrkINV)+sD6f?7xdyBnCd%I<}IA94#gwQsZ+-hXs}j}VZAFkk{v>~ zI~}c$C9eLc_^ss%Lei5oJl&pTgfG&)r(pn^-n(pq#d=QpMuBfwfGkv@0mJo^rVBBT zq|osBR8WvtoRT%U)x}4h!jE&k)O{2NJ>AvkwSwJcCB%hON~XVc?mw zNl>th2g%nKXg2^@80}m?3nZ}0toX6YT2pN1S|+B?VyJZ0SKnz!L`74~SG>n8;2}yP^I4}`0 zaNfr{d!^AVnR%B~dRR1Kb@dTSBa5)+Uj4?bI1cN8PIGo#yO~#yF&nn!lBctjIqN3b zyH%RkHF$`b8(N%-He?Xh%h z|6Q|q*hMwW^4BTo$f}v3+=E!v6EE@Q7xi>mEtiBfeZ~`9z%H)4hxKqRca<$mFL|+iJJYg84i12B2^miUxI}7fLc~#3kmjbFF7xJe6PJYeDK`fSR1Au9hkElL6-bXwkp1uR60`EY-WcTR0BqT-E{eEwI zlg=YsvyYpXr${<@RcJ*O8+qj(Ehq61kcnsw*QSF>8Ac)cEaw$2G3mHlcF6e-bZb6I z${$SS^81(c?pOIw9mMxq8~>@;_@nWJ(0`#8kaY1C71J>jkGnTHFMspx&hUtUGf%+l z?mq1u$f|h|%;kA4iP$gm-P1kfMCw+4lI*dQ(jL%}!EDAUkdbbo#9tg${N?x#_|)CT zf1S$X^LdsHoX~jAn<5C9>Az9!zOK2&i`XcCCEH{RO|79u)A=l3$%L=DtH=Kh;+y+p z5Tp1UcNxOky8lnJQr^4{B!<|u*)Uy>PYf!hVKnl~*yk!qM(A&huVrto@4(~mx;S-> zj7qFpp<1DZG&A{%=S*Q3g*20jl!{aa@c@c(*;U|~;N7=(U`XIt@O=N|yokJE9pRU^ zuUU9DEl#VoTS_ymibLP9OUXyr{p=)<`#yi)Iw6ds_6RHw6zJ+Kk6)@AA%alB>wCV5 zk*!v<^)cC3+IgR0F1EWHMKz4*^(p8+R5PL9Nr9%(71ozlMg7uwMk>49PWeg$g=Kz^ zuWDU6jXq}_72dpNcPsWP*B+xM%_}9lXI~brTVzL9scL6!(*GkdZxgNN2z6x|CZ=%O z9Xfe2G;kx%oQ$*y;J6gZ9Y;c41f^LG7KIf0zpU{8Y)5=-;%L@qG|)?*+mHx*f@*+(VxA262~GKOGS|4QE>pG5xMB@g}46 zcSmBwC(=3?U^1>PzOl!Vm%=O~bk4)>=N=Len@CCN6!EW|iZsgJa-FP6R5vNH;z+O? za#n|-VNweIWyj@jLQ0I=ehC+gO6sqXO}L~6TN%3yxnQdiY}sSc4uM2WOf3|d5XXI3 zO6~?PK_;*h(StK*O$MPnU?cbkXHai#9Lf^8dQ}7dx=`!O>~8!>o_AvZbUagmSTVUU zDa3ZyfnZW1hB6R)_p6>j9es5SS4yLqRjJX63Cg?Jx}*Ir3t8+(C{s!zpD`Rbe5~Cj zIc$cm6dG}n5xLh7boeZMFahT~y>;s_2(SfZ_3WhlR9jJY05%bPiiQ{`$i57EiZVf5 z;Mhs-+UgeV92B*IS^fzG#gFy{9iF;I&-C*D|?mF z^O6b5n>jN9PRjs5&%Vi|W|!GiIY91@A~c-1XfU1%d~=mFZGJP zyNUra!#-NkH~|g&@?cc|R5*s_GJIP8_uW+d22VsyYxl@~OyBlpmNO(O>8LI%4pjEV zL0pV}9L@^I9_rw@tHj;}+E1K##w$TyW^M%Ma*w!o05NW7pwPS9@9v6!^ur7Hf?f|_ zrz8KT_4Y((?Oun8RVX~Pv+h^_W^BlcnX^lK3C>5d_A>d=7raykJrFW!2Pq^?5q9V zzJL(xV>M-=P$(=mgww;4^S5n`==E;u{kT7&%%=FWsC*6hO(i_jPOD0&CwtnyGJ4#| ztMnNT0t%Aed!s7ppKNi~e09f)1?n_jng8Gi=oC6`U#N~M*E3nQON`ksIH5a2e#@V* zL_wE`hGoif$9vF?(+^rvI{NU zM~L6yZo?<+*qq^h__n9Fzry?6{u4cOHPDgjYIS**V(x47Jh(!)r)C>7;qr08l zb0Ndltl{u7s|}yOuVjL}X5Sm#VZQ0j6B$dE&4zYjev5_k326O$&V7y7QL`G2u%gjA z&ujqm>ah6(T8ksXHNl0VVMh6a@MizYz6Tu{3Nq{J;(q+ZS_3lXNwQr8b%HJJzP3|S z9FJktojAJ!vriIr_~z_=ChB4E3Z5zcS&$3*K-pjnijRqCx27b|pI1iwr@qB$QA?R4 z=+ccElGAg;B_dUYsNrU+Ob?f}rj9Cn8Jly4R{9Lwni5G>+ZquJYch%ob_SQValj*; zalLVhYo(tiGZ-w2lfM|YHj$ETA$1f9jwjQ^`J^gyJ14K@TEI3!*3=I53M0@5>&pmT z@}zVTuSZm=yESYSWGCt84OiT%ur;Cq%34XX(q;{2V=7fNW$Nnvggr{tA3G|Z_T$&5 zLyM)$$#3=FaIy}#h9ezDqC}NR*sjTWVA#E%0=7^dt;L_(2J=fu-s;~pUlcDQui&K;aapN7ot!<*dzxd5Cz-#*gxT}Mymc6FM zAIfy?q~*!OFFH397YvV0rj*a9D#No&Pg-<#Md0=5Os9(I_PYGkWEWj%#7kkdiJR96?WNYgzkv?B7{z(T6OyM9aD=Sh&(8&+HRRfyS zU3poaht4HA0gK5PBMIl`MD{ru`#5&VzZ-EVl~1; z9kU_U75ji`Wg(Nf#u%j`mL-{kY0NBkyi_W(R4UrS!t>~REv=cCWcBf}Ok_;kL*41= zJ&zIhg^AvLL%)8+tM~x!j|mB3n4jvfp6niNva$XyM0VPrw-~KeI#HP&H4UmpJaTkToAKlbNOe9ClTwO%8-c~X7vOd#CVb?RloT?ZAe z&E|m*lcvy|t6^OM+gPwQ3?;mAKab{FL>5xUTZW~eKUxg&hfSCwSn$1;*4+jyc%RL* z{2ZN)&#`sZ`6;44^MrwU!mB!yF<(8ZiQ+(6#~0${Ug|w-GdPOqv5P{U`immEwcWfZ z;g;4`X}+DFt(_7lH{JLUw0`I(WABC1Hw{FLVf1WrvMMMXiyM z9uhx5U-PQN--ENgL^rHz#tEA188_QgI&I048duZn_*-jMyrtXk@m1_>;rz|SSp^R7 z#KgoTkG94S>lWo8oe?}Ui{TCt4cj_?(Y)dO(ams%epD&yIshVxkjb z=+qgqqKNypKZiEE6ib>ow0n~AQK-J{*zivYXEQX*t;<}LQ^$J!nXkx#zZX0I zPzx@W+>TT8Lz4c>Z4=!Wqqh@@8jpqHSwWvnZsEAznw%GU_p+Uqa9KeEd6b^*%0~jG zl7!31XI!bGcc6aK|7I3!5id3)Ep+eG?>in+(`F6Tj~ZWL_`AQ>8=76SI*wU?^w7+h2)TKB-q40OZ6{p%4~EyN_D;UdI?*TKhET{ zPn+o#KWR~!`IuDhBZFWE{_tJ0(i1gqpishs$H_EWohz?o(LOZgm`{(kwjjTKZ?A!j zKTQ}e^9!yvwswPH7W%IS&Fnq=y@%^eS}i;G(^i>2_2C_xp0l6uIl=5F)mgK7=O{Ua zd@>%`8x}myoQ7sxNmVYm(FazQ#rmsAV*8ZLOT|f^HqY=Q_rvX4t)dhktS6FkrxGR4 zwb~sHj5p4-Th?uBa||x;J3G}B*(xh}MHq%UipMm{GsL zTR~drj_T+6b68{gJMtLEcsa2@6H?t&F2%wu=s`uo#C&JDj@>`bi4C`UYKM>E8W(2v z&)X}(m|I2Bx8pBsqK$mrPC0DQ6J3^Ne#bOYMwz;>T|Yi)%ejZG6eLx~G@n))r9CZZ ze3FGWT@q3jc)U}L^?z&i6Qymwdf^%BwAh96_7^2#ZJXA$JRO7Dmzwf!Eb7<_!35?i z##yQi1bs1LJF`H+-s9XvcD%-&o;&UpDa(nc0meyH#;%a}N*|#qHP~iGDKw>z(~0=> z)_L^oibw=~9g?t3{UM>kgMVi)nOj8mABDXIf$#7*Fdo?<`>}2(as7_-aju3sJ15u1 zclju~si8lrY-o+h>|}&RQvHiG8#)@kP%hPXeT})E+B|mccUq#>+@7)%%au=HBz1J08y&O!5Ply{= zeeO)nrmNZ><`t(7OG@j8o8))-0tH%-naIF74gt3zhWO4nPS^|*YbB$UTs_GHj^k8SZU!&zmRFcKr=v<59`^QyCQAm^WM(|F)r17z8 zpKiH~qyMHs|D`@mOPbHZ65DcXZ3y65P(=N}L@;(2#KO;2d+> zaQ9)O-14dOo8#Wpw=-|?`?hD(bG4>dn((uiKO6}i^JbI7(R;dR2pt?R?iboxHyo9f#;Q0xa|xu*Q~79a59 z!*WHM=dJksI+)FD^!=q8jO%SXk{ZPFMDu)gnBi2QS4>q+7&oR0b z1Rlv%1s;s{o_R}nXg!->sMWpdr}kbSY@Qzt-DtS@SK1Lh>Ik5`M41at-FHiz>x?SK zpE+_~f|@-5$_;;*KKERpxqDRo>Y(DRbaP=|X!J@g{CTo_!ZB}QO8R5y&%O0Gso%%> zrJH=b-=(bXEhz~2S{}PEUs0~@>K328ZLfT{p2&8W4eM^N7q=h!ZzV??dkv5F>IN?& z4imnZ2lUSVG>q9qnkJ$gdkrj0>&5CJDin}UeR>BJKLs%Q35xlBrMr8})p;>^42b;0 zp?UK9U(x@=fPDM0qOc*WjA-O5CI2jYe0Yo^R%R{OCN1GAmL-d+NkKijV)4_(X@B!x z!t`J+h+VU=(Wi`L8FW0Q!`OLXT9qX;J@|pgxR=>QELf}6R#zH5$@n$%-mmHZZ{z{qYFs^_GgkOC9j!M(t{7}G6H-1arKPPd z75fT=)xkt=Oi&iwpy0vcC!9YC)an^o!nIUJklws1&zZ#r01m_XB9EXo?{}f`u;rw*vaAHvrPhuiGnKX<@ zP6#LX%~(T)H#4I9%1%;ZvS-GV7A#qj6pfk)8F^dTExccYY%_luPW+ zpM5E~Nn9ym5`yepwl8qX4|t!B79nDjg8Bpd@u4!FO_k=)*y++IC`5sCr)c~uCJ0Mr zQlVeDb0N44;h+5PtNf|dCJ8ZQyb(#wzL1_qvZ9oQg4TdOuc9UQ2>8VgR?+ISjnZp@ zDU3&<)_A?1rtzd15_w`J)pn|}up5Hn&|#KMov~2`ZLwzz(oatEEO&y4b;cjcHP$)TtDQ&FA@JM{?(M%D>MD_-YBa z9aX%fCC1erYZA#>#xC@7e5EsY{XJUX*RYj&VwB9?4Ls?}Z_?uTHJP0kYrA&KJUIL? z+em5j-59_yRMAGFk5x45=}q{t;N76!$Zbvoaw=A6E3kaV4Pc_41<{iUuV2T=eNFdq z@1Vu<&$#vam-}IyGgd$u0KNh{m_|59C5>ur+Y*{VmRtMHo_or>E(@8e3c~{9 zRQq|aYjz}QDP{|S$BfI2Ow_nY3keg;dl_^FbB6h$ohpvKlLZWo5;Y`VL*rqfgxo({THnXTr6VGcs_-uR#0ayL-sFV(|GfEwo=o@SW=HA+BH& zMpS=*kknhSsGd0l=I4@HIzq<4$}5Wf=BLU{>Ga6bilR;ED@nf|OjM4YsVXf{`~+pg zHa1z1plp~1`cn)SC>xGK1CJ!3&d7*LSXyZ1j*-!@x0TZP<*KTQ8Vb1GR^bF^xw%+x!o#VPHg)#*}Nug)aPP*pP z%C2E0TLx)-EpYHnbzGibjD!$*=0*gx;KHry)C)H*RCvIcX^jK#ek#ApwU#4`&NyRD zIxj0j-i?5y#cYYJEKreP4F4T|WL)m;yGYnt$=K4VyXHo>b76D~ZTY(KIroxA38XZN z;$;HNhNV?hZZ`b!tD#t&ziQQ7Ojh#{j!gXaQjy2A5t7s;>pQ>DfN%N-c zAOpO00M#6&j@I@auIlpi9J>mIk+)}R5pm5T4z7smt1LEx8p?xaq1>Og;C=-Ce%seY zrC%AP3nNP7^Xp4pp<>dKrlwY>c|V~jR7OgF^`Ur})oP@d{^DbqUy~dW=jTlzt!x!C z7}q&apd#9W;@@7Rapg^u5GiqXFZ%pB^xkiXkM4JE`c{~+zf){&v5T>13sea_r%B`) z-HvMekU%C@H861=NqA$t7K42I2j4-EQD%sjPNr5OtYoZuwj*r0dM~|Nt27EsveRpW zYKS8@TlDy=%oZ<2e^-#Ho+_nIApKAi5i1LC9DMA@8nqSfT~)hJ!I?1PS(OP0iiIr( zvESD21U`~P*iF$+j(w{#G2t%j*pO>XZHs)F)jr$1!<_>G@(Iqw_Z5w7*3;&3Ee zD(1Ho;n57U>k;o~zp*Ts{R{A7bzfqO$l=>^}4rc5j%B{b1yd&;#rj(hJr z2-=8fm%0XRL^QYzxGI>2tSNn$^Dij4gP!f(RxZ|d8`@kApmZb`49!g7JilnZZI?Lb zp4BB$n8!mwR*6Yn)t*M{5*y0~oY9zh6&V}-H%oKy+CPZfpr0AkE3?9s>9^`LcS!HR5$m)UA8DDp@s(}Hoy^r8 zc;K0un@&pkp2|YO13e_!OE{?@==d~f7fulw_`3-Q0fX@2BLu|XBVK<`ctHUus3gp2 zr0l}V&Z#rd$mqmmpc7ukCc&QzFvx{eT#}|SRZZPIk_#J}TfVb$^ed&ziH3w0{cAH0 z41g8{ohU7{C3fA>Ye4c3#J5Rfy{OxX6C-piPEVRgx3?e>y3aNzOcnJB5f}9D%?cm> zELDKNTJ(C>{RZ!NT)MAGEwDRH1aC3Wut&vub+%tzvG;-Oa_G9!!NO(d+@IufI83S( zfAy>X2}l5pGKbe^hwb(5%?=A}$K9P52gb<1=+J!ETui@un3zc=img|kc>bJnc5>PO zOB~IfH&V4bQe%(IbG}zMHHFtyr8`k`na{>AC)5_UKzZzq=c8uV+CVcSKD*kuhLnAz zUn~NsE}RWCkL@>D+g{;prG9G*!J;y)4?XJ3Ya9_jHO~5&gZghfwA zB_-KJR}~G(dtbS<&is>BH#~nzq?K%Js*U?C9i7Z%X=^#3(@`;i;%FVEo}xVo_aRW~ z5c_pDHXX`yAm#OHHSWgUYeca z(`F;?5D)93!nHguL4jqPdRtkL?M87^{?AbyV}mo#=9Lhn+rss8b=X69@zPRIo?a__ zG#q-`Q}Fdug?Sv7VB;bUqQS#A$kk5zvdqSfVO^W#OxN29D0}Unvu!rJT;9daFBP)b z6P4ACo<2bwU*rog`lxZ2S7J%y%v+3!KewNpO%r@*T9->jKdyA4Q8@K24NEr=fyE?# z+WV~6?A$)Xo!=!%m?NK6v*-zUyQoNa%dEKbn#IzWv#@?gOg{1oIvmtiu<6?szQu?0 zNyaRi@Q>l#qAD`=?#gAFRn0@}57^99eFsRkpNb5rY;47@aec~HJ8DvWPb;dDa7q)bUU zg4cxW_{h)ayeDo~9OY>toGUBsP%KJ$P*UZFQVlcwL#q8CJfY!yXhZ#mw)Rd0A9N&z z%|u_)!G8mZc89I-)PZ731rGOqenfU1mTyP$<8_(oz0K0g`Brl4I?2O++HU4MaAtmp z9iLU{?Mqaf7>1(XjG44{CGH%Tm5oSj^BmhSSLt*!rI5m@sZZ_baVuVKnszx)Jw_Kw zrBTtv5MEA8GQ}tFeVV3489$+f5NAz6L039GBb%~H9!?EmRYD)cI}gp6!zX3arC_Rz zhsc|o|FR`<*UMM*COmu>{+vE&!NNM4HPAaTRhfsD_}zmdGm2heMqzffBS40oWiyNy zm+)Aav{ ze3fFYUlH?n2rH}5FO9l`=0iowHntXV7`VA?tTJEJ)fj6FW1m72B(Lqw_PiZDBV3QR zfDav5@YMP3+Ql~p(Oc{y;oCFud+RH*W0lj{tt{I*&#HB19&N#%Hc~otseGo|-Zf7u z=jBDVGqSv9xY8uiOj1>G$2iaMMH-P5PTp{uNyRDmQ6C|YXNFNvJ_I_Nkuf;QLP11!y2;AnL z$r3*~lQNgm$RDcDzoVdcHsPnLOW2Jo^g%azGcU_SUkiRYS@Gq%)+rruqe(VFd>Ly& zIltL2a>ck5@0q6Rl};aBCVlmqoSqu$smInvJ|ZhO=gU7Kcv2?0ZcKJ;+NWy1NMN~s zxSo?c!Do`VktMUy+K!VQISr^Rr}u}z(%r#aqx|ZeAJ=rraU2LGm5JXhE5T_kA=>|w z&Ux#TJIj$?C*Im2IpoFew^TvOsWk81hN2m?pR$?P&kwDtvPCYZiKD`V^1bmJ=zc{4 zI>L=s%hq5&?KONgF{`#Q*NDG5*TuPBE*vL(&?FQcBW3Lr6BI<3AB?Odc2C3EkuFGb zuTGLF*m38x852bkzSxMHY`#o<&3-sJrIK>ZRrphEnp_APB6JI9`m2`lyQM(VZXmH((#-cIQp2|DG}`Uh*jO0d(S2zoJS}ILtnHx4x55aw zUhzdx%Tj-hO9|(@AGOJon&wO{+COcavHCn{8lByK8PXf=zKRDi*@@%7H*T!2U~`P4 zNNt>PVQt!1mYX1J?wOU+TX!{2HOvRrlbX!uksyjIKJ~31uL_(3~HJ(c_`5tBOZ%b5l3e#3yk&tn8^}zzStt`&cAyBZ*6&c5F|A zLf4{GD3Mk(x37F)Hbp_usg7lK=iab>NPJF{Lw!seCZ_g=n3X@4r9E=H&CHQB!%EjP zg#EwyxR@F&Qw;S+{gdD4)CMv<(aMS#k|7o4sBJjX1l)Iqa2@Q%4n_e!jKUUN< z)r5-;)>YJGM9wVA+@otSTo`VTGk1sohqYWHThr?PdjID}MrWR?v&>P{R<%`fdJ*P| zGh*kWwU2&rFvl+J`C;PRqUdYaKvpHXbeF|)jA)no;LTg$J`Jbj)>kkd6864?|A(A@z=3Thq@Y z|1ixzy^PAgM$@VbJJD=1+mmS*O}P4gcBNTdo5bxpn5hi^W>(kbp2@8(w5j--rW1v$ zjnuOASXSP=bp9Yk_Qg5b*1xr}kvxUAJ!-csTud9Bha;FrhrWzHa`;)aF(9}jaC<9h zuBZsZ+(3E!03J+MYgkET^%YjGhU_=x>0%#SWpmLA^38OF#8T*!76(+ zte-haQ|{m|ezZ5oSESK!MwM2Ex0*qhvy`c(n7e*V(yA*z#C$P5-g_{(A!t?lwrYQ-7iduRR;aMvd3UUM zOm?RI4p`N{i}b-ZStD?TIu#v3#WUaTlR7-QB818@ zR}I2M2~(o;&f(Y57iNM_$9MWJWzojU`0Og@kzXLa27=7@tfWrz$cQkO)IVVTyj5?s znc}jSzb-zx5S1i77|iw4KP!EShU9!^iWcE_ z%?PhJvvt3!^e}T5zPewR@GdFb=2$&~+Bj*To?~e(_K&8`x2|>~EOyjT-4@Io{j7rc zNSXSMqg~^?i4(NG(F_t@m#?J1YAH+0;|j{l(xj-OhGGWU6ikyj@#W%6w46~Qtt)oC zD`_d>VMt=F`I=<5$l}m#Ej^7Yxki5?PxBP%SGmz%u*11ES4Ll0@7No17KyeECW3u+ z&m*4YjERO?B@5ff)9#)he`YE#q7a8GN~9j~9MuZZ@#5~8^c}iKEA$?wihXHo5&gZ# z{5>$5i6vhDRBe$yysLC$R-GUK&FKpxES>4~_7T$O&{3$*R1h?jix~1hC6fv6*lAwQ zFDSK#(H@6uocAp81udrnt#QdR+kNljhLU+wjmHp+BEp)cSMmJ4Ci|xbHtGc!NFkeA`gwc+ z3c{MoXh@QbFO#k2CDEtiiaQV8hM0&;xyNt{q$<(b-?*DkYvh>B>WNSkyfoi$EBs3F zEjsTH645+vyPt7x9!2?0y2-?&^wH8PUOE(kOk;MY)JVt?kuY3P6Y~f)PO?7)cwsSg z`AWqrbLe=q0fsX3ifm+I}xRDE$2R8PsM0Ap`Zl%xc3Z!a@mUG zC9COZ>~Lsy+-!CjyEp)_$w4lY*$hbY030xE$RCoh~c zrOCAsoyhkvA)b$J5awVV?_B@lYKNQc7;llA>YVY~WC<3RmU^iiJouDIdI9WUsI5p~ z`we>@-E_^cA2$ep^Y)+2pBfo-^%DXD;_nswe_gT%Lm^=n29%A7nSzmpoRbO~E~l0L zb!Q92DM$jkB&tc~M$|2s9r66@MIvw^Rf$vd>49B|b2K^lp+ajnGsMemt9`B@iWF&T z;n0n+R6}2WkFi?jC*8SdmDDeva99kpt_7Yddf}{XEv`@M!kgeS37buHM-+IeW#8Qk zO8PWX{0=Wad9^fq9W3lgR&G-qb(yMnm=7lZs_7(k zFe+^!GE=vXZov{q8ATr9q|PK9nm>2Llp5l8N<0)C8v-_geH3dh?O0TSo=lb&Ho~K$ zW5XXifdj4t0fn(#!m79*e`N_Tl9f3RcZ0_n#JtPskqJF10bV-u^@}^jI=Bh#Vv$P4 znb-DFep$v!{cqW3mZFzrc~Um3Xk&5E4Fax^1?GqQ;qR$OJ492VyUYCGEZa7bI&h!K z(3O?vVJt^{h@)l_;fs67miZtEBJ*+l?5X)*y$7A zrxagclzW*Fq!6!{eLthtMmG>mRUP{KvNrNqc@%8}Ae@u5%%VjR3V}%l{R03?CG)l; z@E%k}@M2%OG_fjpH|cYooE`(ht*r!pZE6q$iM-Fd!PPe$=1a zLPwODaC5T+;u)pOh{fcPv~aSBvq&4EixZO+oq-9_DR6@ku{>u&6GY8^l;Vw#Yet?K zBZT5$POo-sZgUCj|AZiioms<2>5~3YQm)3vo4U-)B&tm!X_hpN*vYduN#w4Wo`Uo@ zb!2Y5;YQ;?@70Rf;m3ufM$!*TeHimX&;`V1)O`~b-KvRW0>Z;^9ONoucVVsAd5SUa z-A>5axSQarl$+WZtcUT4x(7BSbr7tR&B9M#Co&@qCSXWo!fYJON&;T-LPU^ z{$IFP8PuqeLBhylW)g)I!$>m0YJj>H(9s6}|8(&s;81>F{O>Hx7-EJQ#+EU5Nke3f zv9B>B6ADFQi@RQ4%!dpeAmJ+|EI<+O5Jfe-J%^q;O zOJy-Wsp5Lx@n8vO8l!ShsU7#9$XS}@oxs;leyuz9oWxd_1SshBlt|W=Wm-m!s<|Cj z;D6p2#mR1Q*3JmVwBygmEbJr3O>(OoQ&p|d52`hI5yObZnwyAA%Y3a;4Y?swVcXD9 z1D45sWs%ck_LDeruFr^lm{z-xBFE#;Ee(EjhqEjTF?LLW!JNr_BquY$sH_&Ts>(LFPrnct8wbGUQ$7Y{K! zHQOrPdgt{F?cb8dD42RBs%!892$|V;XrhZ5iXKXj+P)onE#SNClxiz5< zS&ES#K?P#`imL`$Ugo;5A}rqx!E05Ul@>V*3X){^Cm(RdiG)-B5)t64vaWRSm`R#^ zx)ZHJRI20)SK-0=Sc>1+$qC~{?{Lpp9+$v(^!HxZHCo=<5D)kCPWoa|W3Zs13wIM_ zr*TlZpnKsw=nOluT|*IBdqkDSqv@C)r1;ORWO9A-#+b-CSg38ZmcNYuS9g;Q zRioR%>(Y^_I4#-yK3BJu0=jYcL5>-EQe)28!_%IiZiuph#6!!wGH-P%cqQ!G@zJSq z;~#6hIp=yrX9l@HB$eR*Jj`HfpGi<5n`9@tuX)tGLGQJUSiOyQu0X4lmELGj{F03chaW$zFZbh zn${Gu+OVAy_ScQtK3%UZ+d+RsUvdUfSgsPk5T{iLe6y{_a_Z4_9l3(GZQ)+Awe!Y& zz5Z~J4+|SMDyvC;_ zb?&Kdhr*`$mcw;Kmu7C3 zPVi8xu~ZEy#5*~}n4YNAZ29D>#q=N;uF|2M(Om&~IuWOMg>QY*4C-=v{{ePH;)TEJ zq2Qh+GXx?H>=%w;?^LOLE)-*OJhz}dob~#Ag=mzYwwTX@)&Q-Q;Bm32w#3(qp^u7f z<~+Mu7YcLx_kYsW49oNmlTUPngZMHP%W5cPf73rMZRn1XN}tXKs8#d+Ue@9@qg}Xy zCUV_;S5rTECj_Os$qwyeL|~!r8}Jc{W`+s0DWC ztIO=UBOjZ>k5@sVpvE&kuQB&g$&VQOL5Cg{8aPsxVcsm&647a{m+nlpipDr}$twL8 zjfx(<>pHA&`XOHp{yGMl&ot282h&nE4xDz^hs#A0afnKgU5Q~?c@3p=frP2V&?~zE z$_7W6=CNZ+Tv(i>CPA=zi+OCto}Z#E-9~c|NGDoFzm#Q7XraV%>+=SLE|lI)b~k}I zJUr8oBN`6tn9vEy@V+*KHU}D0BY4GA1&a*b%AjX2SKTaI&U#L|Ux$`nq{` z*yv*PE%9r7eP2(}+%h!#v(IT}H}WTl#TNc#|9>I}g(^((2i3;wbnlOygc z`C`i*`}h~XgthP}lVc8}g;*Gi@x$46Qb5{5qS?vSJJOir09x4|7f;<9@yKxUNUXRaf3@?Rkc&8xx9(sY;L+@-!3<(fIFyk@;y$Gk;do7Zk6GL zIiqAil4T|PLvHrl6kZnjoHux&_NZ+`b97x*Gw7q$z0eb3a2lv{Q=*kPZ_x9RCWDfF zEwoJk#v`;pAwY4;9c|vF*z|SO##-;7;pytx@#MM1gAW}VGlZ; zPnvFFpAh5VnTlPkS43Bm@!5)(ZW#Rh)u?y6=jA`93EQcY+gsBThvq(w7ntsXmUpi^ z$Ic~Y6^YpO|1!P)aZBw|3t70AJKX*sF#;@!s7gMuNY>}#ouC9RDg6QPXNc-$8!p#* zq|eVdD2&Fvd^{wjFRx{jj9SpU!5Nu?l!u#yEAQlc5RZggY}kKO>Up4@Vt4+aZIIOY zAd7W}QXa0csMl66!d|7EpxdM2^V|;jtfP&8ZNxScMfriMTG@x7&62ISl*5$YnwiW9EP-S5}8*^hy5n zQp#Lp20c!NKklfm>U=tasz|d}q>VLa;jUt%^QeNfxR)h2dYMJIysR1#?qS|C$pBXR zHJ8U1uaj-=BmL_ZZZ~jcez=M#1Yt0e(5(Dr#(^xdF&gR4l2lWiA=+O2a%8?!(mlh_ zkSh5r+a8r=^?s3;oC0lcOC2Z3FjlQ%z(vCtp#Pv}`C25w9-}fy2A=TxxWwb~;c z%awT}1eTo}4Z~JAu0})u|F&4Jt+|xNhBcccUsjq{Vc5p_*#PC6pzv zWx2M!{{0=A9qaC`14%Xxkm~4euM=-{>*c&v@|l!Uf#o52TA#DY9Fyly1%n$wmQCz4 zQ(7)*LKXWK@D?aJ$(V|Lj|QGDDb`bdAFfVjA-t7S3)>bxuZv@H%9F!=2;Rzujvlsn z*u?g|B7sZ2w+{6$BLMe2SgX#)7LBNc25<`l>< zRod*I)VWd(>=3QLxri!P7y~TIv+??G!%j22{ zX*sIOF}cJm_VCxLhQO0z;g){9?5FO;_HPkE92dg-VhSnt`f;*-DRrmb7}QYEiqNQe z1JbO5R8f-vsr|lrBl1x~UXHZ{lTLGp#G9%>DrM z)fq=eXuU5~aO6g85)7|0KVTpFozLo@%Al)X?frxY~E@??WK|47sU-zmE`nD6^5Lf!$+rC&zmaA2$-3*ioTNq+h1SSUH(Q7wsG+HbI$Qs+)kQa?E~S)S|u(j~jD zdSidWcX1JFn!LDm#t&G>IgbYtZu^~tvwR%1?x2KJX#QPB(#L%th#3K5EP;e&J+9@x;A~%Q(aUDglCLORkXoF-u(a7YtANV5DgpIn8KbRr+{}7KhJ~`slJ&UYZnk!KD1e!;>{WknEw9Qz=uoZc=Tl z`yJ1JHCyef@fl)@o?S~W>$ty?<9{#K{84J4$gs=b`rghWFU(?!Vot7>0^;x@v#@4X zkJRi;P$2?y;5NrdVAn#Ay3^D_oqF=5rQ6SoKDG4 z;#oUV^=1Ej#Bs-6z8)L>sGk*KN!9u>qXB&{rise(3PCAmPxN$;T?Fqm)^@G!ybK?| zH1f=Ci?2!7PMGki!=n!Tp3l!A%DYuJoEXg5A|kf&p%wpp9dlG!I6T?>G5h%wYseZY%M#>q)3N9J2LQy)&zyPnJ6#Vo6=6AC6A?*{%LCZwOK_5Ni{t;CJxsyK@hE4}|v7&0XX-G9SfQ4kdNu zyq$_paBFNaB;(q@-U#Z$UN#(!;@uX}(9&tsDlzy)0N!l3v2;_HcQ0kvWO0Jp*roxCe7O4~=ajZ0BD?2pP)TwE_=UEwdKtzfyG{{Vkv$7G)ZKAdH?IBIo(+9)t z*MPZow+RP&$X}xo$AmlI_9Yte8uOi`GR0r?ERRAZL`-57J=Myv(=W6!HU^jqdk3ZO zIoC5@>juMWH~$9XWs!T1G6(`?AH`$4SM2oHQN|JWrbV~QDI{869tI3xi z|J&dV{>Gc6`H&5z`77JpAwmNtJB90VmVK*;x-Om0H{!md(5DCPadpIFG_Z{FKtpWw z^h2zU^;`3XigAizw9qYG)sP{#+DrHCu;+9MtQ$tQQL)FXpvAAh*B!d6nO%s|(lb8@ zufpN$4pD==j@Ow@lKlO7MB6lkQ!PW+BC{9=IPE4dfTEaW>FLa0BBOILA{M zmYn8iT}R+fnG}lS?aeMT{qd16^3}|*!(DZGQ=QTynq3@?dUO<+@w|}u>|Xee0q2J{ zC?z-sDO+!7QAjle`jzpW(g+;Pi_71GwpK>zqpPja10 z6l2H~$|Z2)hr)>{&+qL6Dl1$emhfR7HqwZU3NO;r!hw5hWD?v)Gc>$PpWBA8LR9aZ z&{w3nQHqc|4u&w$E;mL>B+2)9ds^)HS`>EnTAPgmP3t+>bSa4KuB;FM9D|Pc@3eg? zRBnoW-Dfr`j=ynBbWlkEF-h*X5AaCCMC=}Qym`>FO*XN<@?^sA^Zo&%jjwn#pERnp zR(4N4GYuJ4bZmAd5g){~;2UX=emuJD|A5Qff_EN=x0K5nD}3X-@}mlZcLL;+;Bo#b zXvR|)(yjJ@9NIu0#%zf|O9PQ+f9jg_diGtWPQi@26szBn6+#U4qLS<6&T>1$LnRTM zXWZVsJONKCm}@exdO8%;9TC&4rUG%j2OQ#%G-Oy-xYqC+KJZ~GNLoXY-hsjs+y*yW z5Ly(l+)0R-qxWE}4hYpV#NglLmJ<>J&L0_gNyMw2F&8Vr%|Sp5cTGRK%hShSNbGPr zg~*iLiM`_dnL*Y2Mg#&O-P)tw^BqTmHL{P>Y|?`6gB|SgV=J8(z+Tflw;#Wt&pm`1 zR51>~x_KiQEl3Ax^6C3)yivJgI2?)tg-Wt$4Qxz@0*W|r#iU^XZ-l=fXG@0&@43GI zCxQ`Q$?jbUiZXs?I2IP)2u3HOjZ%L0CtdYB$M%$pbi^Bim&R(nIt%b9i1a}Gpl|?6 zzVl)t=HUET?Zpv{Cp?Qi{n{%k|t=zV z$+G@F2LT%O2x#B?m?qBl5P~yHfPlSB^39&jUftxqrnB(p+ z&`s6E+BV$o$8=X={uz56zEGscgx+bY6csoEaXEHg&Eu4)NLP7Z@kM9lwl^1`oZ%a> zcb;tv1WQ+bQ?+K8Mtw*G;RYX*b<@A3Y?MOn4Pf+pCbWuV0tp~`UNdp+THB&aYK~4k z_G$kPihm~hG5QiQEkPu%>2Rz_5T3g80HYeG_>yjHhi_Il43zHhKdpldxWbfct}?z} zCIWtKTwMa0*_doaPAh!M!z0bSAMl2gH8}zIX=8`Z5wLA93yj0#xfVY~<+2@IB!~h$ z=E68pdvAEZ?XHTvFt-r**{4~+5ml@abS8yP43{-eGkgDlq;`+Llqp25pW%46pG?)r0=1AP!KdgGt4(hpq|ecsCeD*zCs^*E_tTX& zZ(yF*;L8_>^n{X)b?{44Xt}dSO4+ z@ux6&yns#p&6_)>M5H(TFPKfUJUtQA&<+-Iaq;`gv^)o>f^qg<#=931dpTYHXCQ^P zVX?B;OyeH&&p4N@xLSB4%qZkOpzg*F4vVaE@W2-@n~PmbTj<6P_pZMBO=a~Uk!sRI z#FJdVS#y5Zk&Iq-Q@ludzU;pIAemI{y5F;BMSK)59WRIe!DNZ=GC_@@{p-Rwqlq6; z#ZjM2i}X>`T73p z9QLYRNif`$Q}``^9+;$d3lgG1@0g{u7VkVc*3QudH4;c>sRp zaay|`%=U?TRejw$#dsr$scEB_4O!Cy2tS8-F69-EnnwRx$eE~L5&w8Fkw=xUyVLH< zF-UE)JV_kg!6vMix9yqh?_EE(vzW7b9RvvJy^Rf%uZzR^1>Z_TRSCe}UU_ZB$v6a# zP&0DJxp3z81}Si7`rtsAhq|UR>@SFzmW1?T5Bvp>H$)K~l*cp++Qu7v z*5#Pe!wIn8$mif6!{QSfS?5HP zW0Wi`B=#pw7q&C1>G<)$Xe*0UC3!GwJk$Q%k_8LRji_)|!~MsRNKP~uqlj^ztY-N0 z29A99`;>*qlpu>57t3S|4S2(_Rlx0IM7@!WJQ$OTJPU{Xqr|Z18NRTj)*)=QWSFKE ziG;G!LxRZ>^umM+HvqvZ^*;O#nEa zMhBB31Q1UW*vpijF$)u&$2b@g;VK{*A+juVL<Jgcep3d|y%}IRGtK5SfKE zf&`PUyxePsd1H}eMutP7H@awM^k2CM{$;x7Xi;=mk>ySjPP~Tn+6uFaz!VR%SAKfL z4?W9_s8_}PS4nEWB@BY)Wqn2S##2T3K^BFD`^^L!A zgX}dhjOaqNV%UqH|8-jnAd(#n-K`1XavGsns0HOGJV|!R4|uWSBu-_|*gz7d7(0k} zxPDOE+Wob2v!?5I_=exih+VkuJnjz564n{2WK@- zx@RCp;(KaOjj84&M+)p7C479Yq2)>)G4{f+H=RMjoG>R5;0WILhM0l1-mm&@ZPm`U ze1)_O9?pJy7*oM0VpPK)S23?AY;5^Aizzc%%b~z~(Jwl_v{2){<74kC)~bK<;SSrr z)J?q_R8hq5@v!vhAvw!}HAh!1%bA8sXVX`57Kypv_N>Qe>7?9AB}x)mNUDmkWV{=5 z-n2$@Q-m|K)pzMOQb7t`_d?tND&!Tr`bDUGD060vA*zXL54CF7rDB(MX;(0BdVR#8 zb@zq*;!0z3;`7hdafg|Vh-?Q#p{D?)?72H~EkxHvMzub*FFnRzM7@2nf_U>a{MEbS8%)@kB>j;@&$^6x zK?v{t_joz^)bn<8kILc4e@kG^U%g!BVW-hoVn?5s>X|%ENwQmB>gpJV%VGhC4FUgf zVtkTP-lGy}OmRSQ?oMW{Axa`sTh{feHtbl8&f6caOUO{eujuKzvKsX#A?V&yV1fQx z91`q268M~XaP4F*-tzL#w9tt-u|%+vK&Vz#fj0J^$965uoS5stF!jQDUZ;PsaL4B? z&eUu7u>O(>g8%64FJ_bVDeufq2@wW@5vD925aQ+jB5YwUMEu4TR4#1L1MzpR~UUGxuwss1RWtwJt5 zHx+2Qch16LP*ZI4+eyo2x17r7&l$5ZwRC$^bM)lsyt3|_k9;a~d}Md3J%T8NWM9e6 zou=90t^KQuuW2l2s|pNP__i~PmYA&0NdP1muY;kk4sUK=;GL`@l^GFi>Vxk*yM<+G3d z^ss|0A z3KEB~^;}3p`-E6|aSEtg{Cp1_w3zlfGvm9@g|Bcq4?rsjpwlno?8!ZL~|7ynfT@Rn~*eu@0IW4U_38=)HDSA~-6&*biFRZn*v-5Hp zvD=DpZYUr66T|6{vUe0%RWmuMPzGEbyX9$P8Jf>Z^PR@ptpC>S@Gui!8a89-?tUvH zg7IRpgAOIa2ZfqUh3X0uMgNW8@6?0KIfr8P6oz~Q6LXvvcNMr|WpZ|;0z9~@KklPR zgCx$kUgQ=xa7D(IH6Q&=@@kdhi#%*YeOV|QgV|-9_wf|J{EmhBuaEn>G1i6%N_z&I z(~Odha5lLu3*wWQS0Lgh5X1;Cb{7jr4#Ryi*l)IIlQ%BL^O*zhkN-sYPuDy4X1R1V zkjNMvTH4uaovM1SVwNPfS|10ASh4hFn5d6^aXNTI;%U@+Y6|~$F>-xOJ9MY0<}V=p zL#%v?61k3#twFQ1%5Hu$(Kn81kAa0gnGSqDY+meT`TJ_r8E&{x{81zLc}Lz7qlqak zx`E_f#~nRA17dMfXFPn|#9{&_&&KRQ`SGK(o%>en+df`+kUh!Tif@yP@q>QbTy4t| zbCz*%I3bksu7T3+o(c@dK?!a=ECdmy`YsY4c`|NVTYW$57QTtj=5@rG`W`*HWcsRX zxh7}}q<@UC(Exr50@~mi$+vG9QJ3YiRC^n}r_6~Y+w}Oydy7hq`L=qK&crNY;tU3N z_iNrN^VZiVwk%(okiFggGG%33rrLb`ZE1i1TO7W3b5r=aj6QI3 z?OZQ|+HcpALU`Dvh5Xxdz!s2oZ)VjTq!`tqD*Qh-vO_pE3(8Bu=>0sEX0ia~htDrgViY7Rp7imdvnDi)AqL#?lZk(CF z`HpV0Oe|z|(z2Y{eGm4uJn8MNCFsv^6t+CON zP}+|fn}c;`$OXa`zb7z*9&2uM%s00l-N3xC7*axYCE6lJ6dXU2TU7N~HFt+l)cY&s zJjB6}niA=TA519Ba!%XmTV9)we3`q`*Y7l)xL3>0Frrz~idBLCx-!szd-yo~rIm1( zSa~kacpjh~@9yEdGZ#k3GWUVeDZlG>UHw$tKv3Ku#pu8=?W85%xt4_4gDq@PkGU-x zbYC}K@@`^7W^!agqT86;Jcg3N4eg)T@K`Ob0>ikjtbKcq+|BkziIRWndvcp6lBMiz zcODOX3pg|r68hz3hoP_mglQy3_{}QN%pWfe?3+D3<2sgobEtam zLmFhelEJ_Nna6%~@!1}a_0O+2?bufL^!Modfzkp1%h-l`m+Wwy|G~lO4}-sc1JFPR z8_W3uawKv`7Tz?dkz;o5I??#(@WV}dyx0#rKaul_L9b`N*O@%Q7iKI&G&KGrcf6s_ z!HITITyseozCIJMHqw@(J-#uT?+YoLC77y`QJcYn)5QxnlZJxofW)<0Xf-pS(lK>?3#bs{r1CWsu>KEZPt_!7+0rcBFGfZ zw8J?T^rW2(MjC`I82U~tMMdnl688lq7?YlJeN(fT%BOGi?T5P&dIK~c?Dd^c4t;6| z^7bG2>{&ne%s(F!)qZyH&SSdjxYrP!9&=Ugdz4tl-1gvGf`xnZj1s23>$SD|N6C$$vz!a^Gh1C_i)&z};>S}NPP2~Pdc5|#y>^Q1#~B2%aW-v8Lp$Il zI45JAd1TB-=$T{W^N+LCZ%>`~28T-~c>KOEFd6IZJRl5-q0$06spu<@lZw4ucSadS zYd?EEDf?-m{_bV}$1__Gh}U630591cA5CcGQwb1j-NKB1p_?ps+>O*`iuz7TiPkWo zNdM`x7wR$HyeDAoR zgy(WC&{EI8v_e|QQ-}qj+(~d7hRZb_?Rajq6vVx&3zIq9a*FSs_eIGnL>>=bqoaxBuT-8sYocQ}}1W z2{M$QF;_L?es_a-1Id{5&l{;26P3(~nHLi^?x9;QdJR0uFN=C^HWsSPMQP29H#k9t z;+B8j-Bi*bJCi8ZAYUmIy5CU^zD%ind&GwhKTITlaIbXgT|O@m&Uud*6L6sx_2CW5 z2@l`~`Sq(RbjNn!aKZ&%z-~ZRp*!UchPaUs{qgWU`Gd9ll~lJnH_+r4Dp4Oq7vkvA z0P9N>1^enTA3y!XlnWS=9?a>{a_l`_Uu{hn@_ zQr3n3=(U24(j+Bk;Hzv+r%Z`IoUn%d-Cx(ie7H5ko~&R9HnEf_s`|iDXKS-`SK2WC zVm4UfyBVXQ=T`2((;@q*H_59be^A=1{Yw*{z9v@zL323sM3MVJOz{MA{HK->A|dln^->x>gR&my$AMM4MJ z!V-X4kx%5msR7M(noi$$T5!4STM;oyP5@?u(^K5b1CrpFCt7+1`jWU4zF!HfQ_B(9 z!NFH2gsfrWCH&^Pj#BKvU%sb&A$}Kf1bwsA1053|;D%f*1WR<(T14m|9J*%<%;S2H z>2=v8xLWxMu2a42iRB1vfOt4s<+hgLUL8=LP8NoMN_~I1Z|_Xf5I*i$U~q9ygvP~8 z*-3++=}QfhQx&Kc<>6Y^D0-Fe9Gb<uw~uayDH z8(J^naz1j)RJT!mWLc+mXJ`jEiyRX?3r=2gb|rozQ&fIk zXDXPyQu`(?fJ13G$}!-=Y8*Xxn_Md+_Bq2*UR!qqPU@anh$je{_vHMVpY+|hFzu@h zK50_?Ln}5wabWo?ERpC^^(aiYYFQMQug%DvV8-)r*VO(QnR;_=fx;}U=@e&9=8u~* zf?I5PA}og}4wg>fx=%g{sgMLJqoT=pN;G5dwkHn9p5t4--<7{PDXpTuKKT2bQVIAC zBJDRn_~d@}Di-?-)ns-Q(c+ynqa_%B&*@ngAx9YSA>uh%t6*-=`%i1-OUEqY-G7bm z3018|hRzJEAK3WuYi-6@hMK30Evq^@-h1u(Tg}hdxoWarBO!67Tz`|r{2ls-5*70O zL=Napq0@v`?;`X3%Mixk#+d7Og!EFVVNnt%Cj~feRr+YZHGtM!OUjKzF|ud%Ic7O` z%7RpPa}OBn+3ZHqiC_EX$=}4Rl*t)GUjDBAuZ}wyBd~4nzTvqg9Ge%*WIh;T1m^*8 ztL)Dm=%&fGGtwaUwVN+FN8)GOMXw_NiMU>WWV&mh-A@cyBn8j~YqeXD$TIw-OLhNu zZ0`@WR5S)R$Z{-{Efe2?#E;x4}{eV-TEvL-w*E7eb)@wZWZtim@TL%{@mr`i{ z$?68qIv~u~02M1M6Ft97Ul28_9+Lv zY5g>S*1C9jb1d!0xWZNE`upbGe`rI;P;f=Eixxev&WUUERFB3~cjLuCj}x8G+aWW& z(QkS2`Vo95-U*A5_rG&K#|QR_e=3esh=@2{Oodg@sd9solU>|<;u!Y}#fI!hW&SP3 z&h#wv-l_t(r@znsdu* zds@1AB^gGtK(iNf$<#b&q~zePIN=xley-DVg|Gbp>x!~t3IF9aBr*Enmst6j3>&cI zos|iveoRh^82$1vONY|$XYLzY8O2e(%{|sB^p2M4YzZ+iS)B$R8$26LNPRU@wKuDx z(mb+$z^Jb2lA@cnLRMrcS^GI{>CY?NI!4t(U|c>BP2UjkEBW`o#sTutn!DWp{Y^5T zA(S(O_T~@DceZ5VHWH@Ag1hb^ATwaKjD>(qWMly2t&C4T3cdsng*czUmna59Ry4bQ zu36t(NB~(dO#5M6aVT4_B=dD81yVRz!5tDN{sE_`%+)^135pggx3wU)C<%_@u!SMo zOc+**ZmoUMkXb*x#tHWaA&TT|EJ7B5rb@5 znC9-Pm?oUPp$gFs;Xh$c#B4+Gdz-txMQm`nTF*pRjJijeWs7+$j^AE}$I1^Lqgu(< z4U2lQiUop$JrrNl1vswHRw18tc^)ij zZ6YafhrCZy@UJLGI*TmZzzcaOE>jgMgwZ)$IFz-4eIJS_zRW_vA0g@;48KxVETNR@ zn;ZxA_#kP354TaE+E=Unm5!h$LEBGO+j4Y` zaJM4~4&rm3t@sjYv9Fr?38J2dM%FofuJY4u%kA&7$?eL=@4B%*0noR2S|mW#gSh_1UFZfg`HW&zPB$B ziEkYx*=crN63_v|`m(f(9ZqOdc?xqDfWNOjhb}?8%b6S9 z+qeMZwvKz3gFt*12|qMQZC&3ols_rOfU1=FP4qi+di0qMqz{seoV1B6_;jafK&-q% zBpjN@$!-CBMn;CP8*x|~F_KwzgYRxkBVS&7q)fC0^T)Pll?7KJ&xe`ha)%=ZwU^XcUGAAYI5#- zhb$#8Q+Ap-fB{bEuddh78NjzzZDE#38YFGYFdc+bgsCI{1A2{QoOrZaV~w|O zhv95=YZCzN7m=SMzaqDgN4oz)>~&zn>|*({|4-0F(|JHDf!=&PRTmL9T$;^^l?UE3 zJ&8RS4vdIGa*V;3<9%l?gE|{Oc9*XLAaVg}Fyx#90Y^|E>dHGe5z$zcrVnKafR74! zJV+22g=*o|N+afJs(}xX;YTD+U;W-e<*Y2OzIM)aRPW~|hir4e&pDU!u;k~%V*C|N zDL~Xc0tE&gFfm)5fiEjUI$?L&b|ymqvM>z)fCf~CW5v=`aje+m{Bc?dQVAMvQfwn^ zTtMpy30Mj#qENQ;i1Me5oj|o#haiZ2^gl(Ryw`@r%>bU G-TwjAB&N^+ literal 0 HcmV?d00001 diff --git a/switch/src/gui.cpp b/switch/src/gui.cpp index 2d0b71f..c23f470 100644 --- a/switch/src/gui.cpp +++ b/switch/src/gui.cpp @@ -281,7 +281,7 @@ bool MainApplication::Load() // Create a view this->rootFrame = new brls::TabFrame(); this->rootFrame->setTitle("Chiaki: Open Source PlayStation Remote Play Client"); - this->rootFrame->setIcon(BOREALIS_ASSET("icon.jpg")); + this->rootFrame->setIcon(BOREALIS_ASSET("icon.png")); brls::List *config = new brls::List(); brls::List *add_host = new brls::List(); From 2a4b67b58e4c2d9ec01f10db7fcc0a681c1ff6e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Mon, 11 Jan 2021 20:13:43 +0100 Subject: [PATCH 172/237] Complete ControllerState in JNI --- android/app/src/main/cpp/chiaki-jni.c | 58 ++++++++++ .../java/com/metallic/chiaki/lib/Chiaki.kt | 103 ++++++++++++++++-- 2 files changed, 152 insertions(+), 9 deletions(-) diff --git a/android/app/src/main/cpp/chiaki-jni.c b/android/app/src/main/cpp/chiaki-jni.c index 49229f5..72b6af4 100644 --- a/android/app/src/main/cpp/chiaki-jni.c +++ b/android/app/src/main/cpp/chiaki-jni.c @@ -140,6 +140,20 @@ typedef struct android_chiaki_session_t jfieldID java_controller_state_left_y; jfieldID java_controller_state_right_x; jfieldID java_controller_state_right_y; + jfieldID java_controller_state_touches; + jfieldID java_controller_state_gyro_x; + jfieldID java_controller_state_gyro_y; + jfieldID java_controller_state_gyro_z; + jfieldID java_controller_state_accel_x; + jfieldID java_controller_state_accel_y; + jfieldID java_controller_state_accel_z; + jfieldID java_controller_state_orient_x; + jfieldID java_controller_state_orient_y; + jfieldID java_controller_state_orient_z; + jfieldID java_controller_state_orient_w; + jfieldID java_controller_touch_x; + jfieldID java_controller_touch_y; + jfieldID java_controller_touch_id; AndroidChiakiVideoDecoder video_decoder; AndroidChiakiAudioDecoder audio_decoder; @@ -305,6 +319,22 @@ JNIEXPORT void JNICALL JNI_FCN(sessionCreate)(JNIEnv *env, jobject obj, jobject session->java_controller_state_left_y = E->GetFieldID(env, controller_state_class, "leftY", "S"); session->java_controller_state_right_x = E->GetFieldID(env, controller_state_class, "rightX", "S"); session->java_controller_state_right_y = E->GetFieldID(env, controller_state_class, "rightY", "S"); + session->java_controller_state_touches = E->GetFieldID(env, controller_state_class, "touches", "[L"BASE_PACKAGE"/ControllerTouch;"); + session->java_controller_state_gyro_x = E->GetFieldID(env, controller_state_class, "gyroX", "F"); + session->java_controller_state_gyro_y = E->GetFieldID(env, controller_state_class, "gyroY", "F"); + session->java_controller_state_gyro_z = E->GetFieldID(env, controller_state_class, "gyroZ", "F"); + session->java_controller_state_accel_x = E->GetFieldID(env, controller_state_class, "accelX", "F"); + session->java_controller_state_accel_y = E->GetFieldID(env, controller_state_class, "accelY", "F"); + session->java_controller_state_accel_z = E->GetFieldID(env, controller_state_class, "accelZ", "F"); + session->java_controller_state_orient_x = E->GetFieldID(env, controller_state_class, "orientX", "F"); + session->java_controller_state_orient_y = E->GetFieldID(env, controller_state_class, "orientY", "F"); + session->java_controller_state_orient_z = E->GetFieldID(env, controller_state_class, "orientZ", "F"); + session->java_controller_state_orient_w = E->GetFieldID(env, controller_state_class, "orientW", "F"); + + jclass controller_touch_class = E->FindClass(env, BASE_PACKAGE"/ControllerTouch"); + session->java_controller_touch_x = E->GetFieldID(env, controller_touch_class, "x", "S"); + session->java_controller_touch_y = E->GetFieldID(env, controller_touch_class, "y", "S"); + session->java_controller_touch_id = E->GetFieldID(env, controller_touch_class, "id", "B"); chiaki_session_set_event_cb(&session->session, android_chiaki_event_cb, session); chiaki_session_set_video_sample_cb(&session->session, android_chiaki_video_decoder_video_sample, &session->video_decoder); @@ -382,6 +412,34 @@ JNIEXPORT void JNICALL JNI_FCN(sessionSetControllerState)(JNIEnv *env, jobject o controller_state.left_y = (int16_t)E->GetShortField(env, controller_state_java, session->java_controller_state_left_y); controller_state.right_x = (int16_t)E->GetShortField(env, controller_state_java, session->java_controller_state_right_x); controller_state.right_y = (int16_t)E->GetShortField(env, controller_state_java, session->java_controller_state_right_y); + jobjectArray touch_array = E->GetObjectField(env, controller_state_java, session->java_controller_state_touches); + size_t touch_array_len = (size_t)E->GetArrayLength(env, touch_array); + for(size_t i = 0; i < CHIAKI_CONTROLLER_TOUCHES_MAX; i++) + { + if(i < touch_array_len) + { + jobject touch = E->GetObjectArrayElement(env, touch_array, i); + controller_state.touches[i].x = (uint16_t)E->GetShortField(env, touch, session->java_controller_touch_x); + controller_state.touches[i].y = (uint16_t)E->GetShortField(env, touch, session->java_controller_touch_y); + controller_state.touches[i].id = (int8_t)E->GetByteField(env, touch, session->java_controller_touch_id); + } + else + { + controller_state.touches[i].x = 0; + controller_state.touches[i].y = 0; + controller_state.touches[i].id = -1; + } + } + controller_state.gyro_x = E->GetFloatField(env, controller_state_java, session->java_controller_state_gyro_x); + controller_state.gyro_y = E->GetFloatField(env, controller_state_java, session->java_controller_state_gyro_y); + controller_state.gyro_z = E->GetFloatField(env, controller_state_java, session->java_controller_state_gyro_z); + controller_state.accel_x = E->GetFloatField(env, controller_state_java, session->java_controller_state_accel_x); + controller_state.accel_y = E->GetFloatField(env, controller_state_java, session->java_controller_state_accel_y); + controller_state.accel_z = E->GetFloatField(env, controller_state_java, session->java_controller_state_accel_z); + controller_state.orient_x = E->GetFloatField(env, controller_state_java, session->java_controller_state_orient_x); + controller_state.orient_y = E->GetFloatField(env, controller_state_java, session->java_controller_state_orient_y); + controller_state.orient_z = E->GetFloatField(env, controller_state_java, session->java_controller_state_orient_z); + controller_state.orient_w = E->GetFloatField(env, controller_state_java, session->java_controller_state_orient_w); chiaki_session_set_controller_state(&session->session, &controller_state); } diff --git a/android/app/src/main/java/com/metallic/chiaki/lib/Chiaki.kt b/android/app/src/main/java/com/metallic/chiaki/lib/Chiaki.kt index d638cb2..0700a7c 100644 --- a/android/app/src/main/java/com/metallic/chiaki/lib/Chiaki.kt +++ b/android/app/src/main/java/com/metallic/chiaki/lib/Chiaki.kt @@ -150,6 +150,14 @@ class ChiakiLog(val levelMask: Int, val callback: (level: Int, text: String) -> private fun maxAbs(a: Short, b: Short) = if(abs(a.toInt()) > abs(b.toInt())) a else b +private val CONTROLLER_TOUCHES_MAX = 2 // must be the same as CHIAKI_CONTROLLER_TOUCHES_MAX + +data class ControllerTouch( + val x: UShort = 0U, + val y: UShort = 0U, + val id: Byte = -1 // -1 = up +) + data class ControllerState constructor( var buttons: UInt = 0U, var l2State: UByte = 0U, @@ -157,25 +165,37 @@ data class ControllerState constructor( var leftX: Short = 0, var leftY: Short = 0, var rightX: Short = 0, - var rightY: Short = 0 + var rightY: Short = 0, + private var touchIdNext: UByte = 0U, + var touches: Array = arrayOf(ControllerTouch(), ControllerTouch()), + var gyroX: Float = 0.0f, + var gyroY: Float = 0.0f, + var gyroZ: Float = 0.0f, + var accelX: Float = 0.0f, + var accelY: Float = 1.0f, + var accelZ: Float = 0.0f, + var orientX: Float = 0.0f, + var orientY: Float = 0.0f, + var orientZ: Float = 0.0f, + var orientW: Float = 1.0f ){ companion object { val BUTTON_CROSS = (1 shl 0).toUInt() val BUTTON_MOON = (1 shl 1).toUInt() - val BUTTON_BOX = (1 shl 2).toUInt() - val BUTTON_PYRAMID = (1 shl 3).toUInt() + val BUTTON_BOX = (1 shl 2).toUInt() + val BUTTON_PYRAMID = (1 shl 3).toUInt() val BUTTON_DPAD_LEFT = (1 shl 4).toUInt() val BUTTON_DPAD_RIGHT = (1 shl 5).toUInt() - val BUTTON_DPAD_UP = (1 shl 6).toUInt() + val BUTTON_DPAD_UP = (1 shl 6).toUInt() val BUTTON_DPAD_DOWN = (1 shl 7).toUInt() - val BUTTON_L1 = (1 shl 8).toUInt() - val BUTTON_R1 = (1 shl 9).toUInt() + val BUTTON_L1 = (1 shl 8).toUInt() + val BUTTON_R1 = (1 shl 9).toUInt() val BUTTON_L3 = (1 shl 10).toUInt() val BUTTON_R3 = (1 shl 11).toUInt() - val BUTTON_OPTIONS = (1 shl 12).toUInt() + val BUTTON_OPTIONS = (1 shl 12).toUInt() val BUTTON_SHARE = (1 shl 13).toUInt() - val BUTTON_TOUCHPAD = (1 shl 14).toUInt() + val BUTTON_TOUCHPAD = (1 shl 14).toUInt() val BUTTON_PS = (1 shl 15).toUInt() } @@ -186,8 +206,73 @@ data class ControllerState constructor( leftX = maxAbs(leftX, o.leftX), leftY = maxAbs(leftY, o.leftY), rightX = maxAbs(rightX, o.rightX), - rightY = maxAbs(rightY, o.rightY) + rightY = maxAbs(rightY, o.rightY), + touches = touches.zip(o.touches) { a, b -> if(a.id >= 0) a else b }.toTypedArray(), + gyroX = gyroX, + gyroY = gyroY, + gyroZ = gyroZ, + accelX = accelX, + accelY = accelY, + accelZ = accelZ, + orientX = orientX, + orientY = orientY, + orientZ = orientZ, + orientW = orientW ) + + override fun equals(other: Any?): Boolean + { + if(this === other) return true + if(javaClass != other?.javaClass) return false + + other as ControllerState + + if(buttons != other.buttons) return false + if(l2State != other.l2State) return false + if(r2State != other.r2State) return false + if(leftX != other.leftX) return false + if(leftY != other.leftY) return false + if(rightX != other.rightX) return false + if(rightY != other.rightY) return false + if(touchIdNext != other.touchIdNext) return false + if(!touches.contentEquals(other.touches)) return false + if(gyroX != other.gyroX) return false + if(gyroY != other.gyroY) return false + if(gyroZ != other.gyroZ) return false + if(accelX != other.accelX) return false + if(accelY != other.accelY) return false + if(accelZ != other.accelZ) return false + if(orientX != other.orientX) return false + if(orientY != other.orientY) return false + if(orientZ != other.orientZ) return false + if(orientW != other.orientW) return false + + return true + } + + override fun hashCode(): Int + { + var result = buttons.hashCode() + result = 31 * result + l2State.hashCode() + result = 31 * result + r2State.hashCode() + result = 31 * result + leftX + result = 31 * result + leftY + result = 31 * result + rightX + result = 31 * result + rightY + result = 31 * result + touchIdNext.hashCode() + result = 31 * result + touches.contentHashCode() + result = 31 * result + gyroX.hashCode() + result = 31 * result + gyroY.hashCode() + result = 31 * result + gyroZ.hashCode() + result = 31 * result + accelX.hashCode() + result = 31 * result + accelY.hashCode() + result = 31 * result + accelZ.hashCode() + result = 31 * result + orientX.hashCode() + result = 31 * result + orientY.hashCode() + result = 31 * result + orientZ.hashCode() + result = 31 * result + orientW.hashCode() + return result + } } class QuitReason(val value: Int) From 2906cfdd69ff0343c10bada4362387cc0627424a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Tue, 12 Jan 2021 21:08:33 +0100 Subject: [PATCH 173/237] Add Motion to Android --- .../metallic/chiaki/session/StreamInput.kt | 89 +++++++++++++++++-- .../metallic/chiaki/stream/StreamActivity.kt | 6 +- .../metallic/chiaki/stream/StreamViewModel.kt | 13 +-- 3 files changed, 95 insertions(+), 13 deletions(-) diff --git a/android/app/src/main/java/com/metallic/chiaki/session/StreamInput.kt b/android/app/src/main/java/com/metallic/chiaki/session/StreamInput.kt index e96b7b5..2518ce1 100644 --- a/android/app/src/main/java/com/metallic/chiaki/session/StreamInput.kt +++ b/android/app/src/main/java/com/metallic/chiaki/session/StreamInput.kt @@ -1,19 +1,36 @@ package com.metallic.chiaki.session -import android.util.Log -import android.view.InputDevice -import android.view.KeyEvent -import android.view.MotionEvent +import android.content.Context +import android.hardware.* +import android.view.* +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleObserver +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.OnLifecycleEvent import com.metallic.chiaki.common.Preferences import com.metallic.chiaki.lib.ControllerState -class StreamInput(val preferences: Preferences) +class StreamInput(val context: Context, val preferences: Preferences) { var controllerStateChangedCallback: ((ControllerState) -> Unit)? = null val controllerState: ControllerState get() { - val controllerState = keyControllerState or motionControllerState + val controllerState = sensorControllerState or keyControllerState or motionControllerState + + val windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager + when(windowManager.defaultDisplay.rotation) + { + Surface.ROTATION_90 -> { + controllerState.accelX *= -1.0f + controllerState.accelZ *= -1.0f + controllerState.gyroX *= -1.0f + controllerState.gyroZ *= -1.0f + controllerState.orientX *= -1.0f + controllerState.orientZ *= -1.0f + } + else -> {} + } // prioritize motion controller's l2 and r2 over key // (some controllers send only key, others both but key earlier than full press) @@ -25,6 +42,7 @@ class StreamInput(val preferences: Preferences) return controllerState or touchControllerState } + private val sensorControllerState = ControllerState() // from Motion Sensors private val keyControllerState = ControllerState() // from KeyEvents private val motionControllerState = ControllerState() // from MotionEvents var touchControllerState = ControllerState() @@ -36,6 +54,65 @@ class StreamInput(val preferences: Preferences) private val swapCrossMoon = preferences.swapCrossMoon + private val sensorEventListener = object: SensorEventListener { + override fun onSensorChanged(event: SensorEvent) + { + when(event.sensor.type) + { + Sensor.TYPE_ACCELEROMETER -> { + sensorControllerState.accelX = event.values[1] / SensorManager.GRAVITY_EARTH + sensorControllerState.accelY = event.values[2] / SensorManager.GRAVITY_EARTH + sensorControllerState.accelZ = event.values[0] / SensorManager.GRAVITY_EARTH + } + Sensor.TYPE_GYROSCOPE -> { + sensorControllerState.gyroX = event.values[1] + sensorControllerState.gyroY = event.values[2] + sensorControllerState.gyroZ = event.values[0] + } + Sensor.TYPE_ROTATION_VECTOR -> { + val q = floatArrayOf(0f, 0f, 0f, 0f) + SensorManager.getQuaternionFromVector(q, event.values) + sensorControllerState.orientX = q[2] + sensorControllerState.orientY = q[3] + sensorControllerState.orientZ = q[1] + sensorControllerState.orientW = q[0] + } + else -> return + } + controllerStateUpdated() + } + + override fun onAccuracyChanged(sensor: Sensor, accuracy: Int) {} + } + + private val lifecycleObserver = object: LifecycleObserver { + @OnLifecycleEvent(Lifecycle.Event.ON_RESUME) + fun onResume() + { + val samplingPeriodUs = 4000 + val sensorManager = context.getSystemService(Context.SENSOR_SERVICE) as SensorManager + listOfNotNull( + sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER), + sensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE), + sensorManager.getDefaultSensor(Sensor.TYPE_ROTATION_VECTOR) + ).forEach { + sensorManager.registerListener(sensorEventListener, it, samplingPeriodUs) + } + } + + @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE) + fun onPause() + { + val sensorManager = context.getSystemService(Context.SENSOR_SERVICE) as SensorManager + sensorManager.unregisterListener(sensorEventListener) + } + } + + fun observe(lifecycleOwner: LifecycleOwner) + { + lifecycleOwner.lifecycle.addObserver(lifecycleObserver) + } + private fun controllerStateUpdated() { controllerStateChangedCallback?.let { it(controllerState) } diff --git a/android/app/src/main/java/com/metallic/chiaki/stream/StreamActivity.kt b/android/app/src/main/java/com/metallic/chiaki/stream/StreamActivity.kt index 67d425b..903b408 100644 --- a/android/app/src/main/java/com/metallic/chiaki/stream/StreamActivity.kt +++ b/android/app/src/main/java/com/metallic/chiaki/stream/StreamActivity.kt @@ -5,6 +5,7 @@ package com.metallic.chiaki.stream import android.animation.Animator import android.animation.AnimatorListenerAdapter import android.app.AlertDialog +import android.content.res.Configuration import android.graphics.Matrix import android.os.Bundle import android.os.Handler @@ -58,9 +59,11 @@ class StreamActivity : AppCompatActivity(), View.OnSystemUiVisibilityChangeListe } viewModel = ViewModelProvider(this, viewModelFactory { - StreamViewModel(Preferences(this), LogManager(this), connectInfo) + StreamViewModel(application, connectInfo) })[StreamViewModel::class.java] + viewModel.input.observe(this) + setContentView(R.layout.activity_stream) window.decorView.setOnSystemUiVisibilityChangeListener(this) @@ -305,7 +308,6 @@ class StreamActivity : AppCompatActivity(), View.OnSystemUiVisibilityChangeListe override fun dispatchKeyEvent(event: KeyEvent) = viewModel.input.dispatchKeyEvent(event) || super.dispatchKeyEvent(event) override fun onGenericMotionEvent(event: MotionEvent) = viewModel.input.onGenericMotionEvent(event) || super.onGenericMotionEvent(event) - } diff --git a/android/app/src/main/java/com/metallic/chiaki/stream/StreamViewModel.kt b/android/app/src/main/java/com/metallic/chiaki/stream/StreamViewModel.kt index ebaaf80..df69378 100644 --- a/android/app/src/main/java/com/metallic/chiaki/stream/StreamViewModel.kt +++ b/android/app/src/main/java/com/metallic/chiaki/stream/StreamViewModel.kt @@ -2,19 +2,22 @@ package com.metallic.chiaki.stream -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel +import android.app.Application +import android.content.Context +import androidx.lifecycle.* import com.metallic.chiaki.common.LogManager import com.metallic.chiaki.session.StreamSession import com.metallic.chiaki.common.Preferences import com.metallic.chiaki.lib.* import com.metallic.chiaki.session.StreamInput -class StreamViewModel(val preferences: Preferences, val logManager: LogManager, val connectInfo: ConnectInfo): ViewModel() +class StreamViewModel(val application: Application, val connectInfo: ConnectInfo): ViewModel() { + val preferences = Preferences(application) + val logManager = LogManager(application) + private var _session: StreamSession? = null - val input = StreamInput(preferences) + val input = StreamInput(application, preferences) val session = StreamSession(connectInfo, logManager, preferences.logVerbose, input) private var _onScreenControlsEnabled = MutableLiveData(preferences.onScreenControlsEnabled) From 3b85e147b6056640f17178a1336e1c9f0be6c755 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Wed, 13 Jan 2021 15:00:53 +0100 Subject: [PATCH 174/237] Add Rumble to Android --- README.md | 12 ++---------- android/app/src/main/AndroidManifest.xml | 1 + android/app/src/main/cpp/chiaki-jni.c | 10 ++++++++++ .../com/metallic/chiaki/common/Preferences.kt | 7 ++++++- .../main/java/com/metallic/chiaki/lib/Chiaki.kt | 6 ++++++ .../com/metallic/chiaki/session/StreamSession.kt | 3 +++ .../metallic/chiaki/settings/SettingsFragment.kt | 2 ++ .../com/metallic/chiaki/stream/StreamActivity.kt | 16 ++++++++++++++++ android/app/src/main/res/drawable/ic_rumble.xml | 9 +++++++++ android/app/src/main/res/values/strings.xml | 5 ++++- android/app/src/main/res/xml/preferences.xml | 6 ++++++ 11 files changed, 65 insertions(+), 12 deletions(-) create mode 100644 android/app/src/main/res/drawable/ic_rumble.xml diff --git a/README.md b/README.md index b84b73d..5129f3a 100644 --- a/README.md +++ b/README.md @@ -12,17 +12,9 @@ for Linux, FreeBSD, OpenBSD, Android, macOS, Windows, Nintendo Switch and potent ![Screenshot](assets/screenshot.png) -## Features - -Everything necessary for a full streaming session, including the initial -registration and wakeup of the console, is supported. -The following features however are yet to be implemented: -* Rumble -* Accelerometer/Gyroscope - ## Installing -You can either download a pre-built release (easier) or build Chiaki from source. +You can either download a pre-built release or build Chiaki from source. ### Downloading a Release @@ -48,7 +40,7 @@ make ``` For more detailed platform-specific instructions, see [doc/platform-build.md](doc/platform-build.md). -in + ## Usage If your Console is on your local network, is turned on or in standby mode and does not have Discovery explicitly disabled, Chiaki should find it. diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 14b0466..fa14662 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -5,6 +5,7 @@ + CallVoidMethod(env, session->java_session, + session->java_session_event_rumble_meth, + (jint)event->rumble.left, + (jint)event->rumble.right); + break; + default: + break; } (*global_vm)->DetachCurrentThread(global_vm); @@ -310,6 +319,7 @@ JNIEXPORT void JNICALL JNI_FCN(sessionCreate)(JNIEnv *env, jobject obj, jobject session->java_session_event_connected_meth = E->GetMethodID(env, session->java_session_class, "eventConnected", "()V"); session->java_session_event_login_pin_request_meth = E->GetMethodID(env, session->java_session_class, "eventLoginPinRequest", "(Z)V"); session->java_session_event_quit_meth = E->GetMethodID(env, session->java_session_class, "eventQuit", "(ILjava/lang/String;)V"); + session->java_session_event_rumble_meth = E->GetMethodID(env, session->java_session_class, "eventRumble", "(II)V"); jclass controller_state_class = E->FindClass(env, BASE_PACKAGE"/ControllerState"); session->java_controller_state_buttons = E->GetFieldID(env, controller_state_class, "buttons", "I"); diff --git a/android/app/src/main/java/com/metallic/chiaki/common/Preferences.kt b/android/app/src/main/java/com/metallic/chiaki/common/Preferences.kt index c9691b5..d779c5a 100644 --- a/android/app/src/main/java/com/metallic/chiaki/common/Preferences.kt +++ b/android/app/src/main/java/com/metallic/chiaki/common/Preferences.kt @@ -68,11 +68,16 @@ class Preferences(context: Context) get() = sharedPreferences.getBoolean(onScreenControlsEnabledKey, true) set(value) { sharedPreferences.edit().putBoolean(onScreenControlsEnabledKey, value).apply() } - val touchpadOnlyEnabledKey get() = resources.getString(R.string.preferences_touchpad_only_key) + val touchpadOnlyEnabledKey get() = resources.getString(R.string.preferences_touchpad_only_enabled_key) var touchpadOnlyEnabled get() = sharedPreferences.getBoolean(touchpadOnlyEnabledKey, false) set(value) { sharedPreferences.edit().putBoolean(touchpadOnlyEnabledKey, value).apply() } + val rumbleEnabledKey get() = resources.getString(R.string.preferences_rumble_enabled_key) + var rumbleEnabled + get() = sharedPreferences.getBoolean(rumbleEnabledKey, true) + set(value) { sharedPreferences.edit().putBoolean(rumbleEnabledKey, value).apply() } + val logVerboseKey get() = resources.getString(R.string.preferences_log_verbose_key) var logVerbose get() = sharedPreferences.getBoolean(logVerboseKey, false) diff --git a/android/app/src/main/java/com/metallic/chiaki/lib/Chiaki.kt b/android/app/src/main/java/com/metallic/chiaki/lib/Chiaki.kt index 0700a7c..023da90 100644 --- a/android/app/src/main/java/com/metallic/chiaki/lib/Chiaki.kt +++ b/android/app/src/main/java/com/metallic/chiaki/lib/Chiaki.kt @@ -289,6 +289,7 @@ sealed class Event object ConnectedEvent: Event() data class LoginPinRequestEvent(val pinIncorrect: Boolean): Event() data class QuitEvent(val reason: QuitReason, val reasonString: String?): Event() +data class RumbleEvent(val left: UByte, val right: UByte): Event() class CreateError(val errorCode: ErrorCode): Exception("Failed to create a native object: $errorCode") @@ -344,6 +345,11 @@ class Session(connectInfo: ConnectInfo, logFile: String?, logVerbose: Boolean) event(QuitEvent(QuitReason(reasonValue), reasonString)) } + private fun eventRumble(left: Int, right: Int) + { + event(RumbleEvent(left.toUByte(), right.toUByte())) + } + fun setSurface(surface: Surface) { ChiakiNative.sessionSetSurface(nativePtr, surface) diff --git a/android/app/src/main/java/com/metallic/chiaki/session/StreamSession.kt b/android/app/src/main/java/com/metallic/chiaki/session/StreamSession.kt index 059840c..d7f127f 100644 --- a/android/app/src/main/java/com/metallic/chiaki/session/StreamSession.kt +++ b/android/app/src/main/java/com/metallic/chiaki/session/StreamSession.kt @@ -25,6 +25,8 @@ class StreamSession(val connectInfo: ConnectInfo, val logManager: LogManager, va private val _state = MutableLiveData(StreamStateIdle) val state: LiveData get() = _state + private val _rumbleState = MutableLiveData(RumbleEvent(0U, 0U)) + val rumbleState: LiveData get() = _rumbleState var surfaceTexture: SurfaceTexture? = null @@ -86,6 +88,7 @@ class StreamSession(val connectInfo: ConnectInfo, val logManager: LogManager, va event.pinIncorrect ) ) + is RumbleEvent -> _rumbleState.postValue(event) } } diff --git a/android/app/src/main/java/com/metallic/chiaki/settings/SettingsFragment.kt b/android/app/src/main/java/com/metallic/chiaki/settings/SettingsFragment.kt index f38f619..7a4506e 100644 --- a/android/app/src/main/java/com/metallic/chiaki/settings/SettingsFragment.kt +++ b/android/app/src/main/java/com/metallic/chiaki/settings/SettingsFragment.kt @@ -26,6 +26,7 @@ class DataStore(val preferences: Preferences): PreferenceDataStore() { preferences.logVerboseKey -> preferences.logVerbose preferences.swapCrossMoonKey -> preferences.swapCrossMoon + preferences.rumbleEnabledKey -> preferences.rumbleEnabled else -> defValue } @@ -35,6 +36,7 @@ class DataStore(val preferences: Preferences): PreferenceDataStore() { preferences.logVerboseKey -> preferences.logVerbose = value preferences.swapCrossMoonKey -> preferences.swapCrossMoon = value + preferences.rumbleEnabledKey -> preferences.rumbleEnabled = value } } diff --git a/android/app/src/main/java/com/metallic/chiaki/stream/StreamActivity.kt b/android/app/src/main/java/com/metallic/chiaki/stream/StreamActivity.kt index 903b408..d94fa0d 100644 --- a/android/app/src/main/java/com/metallic/chiaki/stream/StreamActivity.kt +++ b/android/app/src/main/java/com/metallic/chiaki/stream/StreamActivity.kt @@ -9,6 +9,8 @@ import android.content.res.Configuration import android.graphics.Matrix import android.os.Bundle import android.os.Handler +import android.os.VibrationEffect +import android.os.Vibrator import android.view.KeyEvent import android.view.MotionEvent import android.view.TextureView @@ -30,6 +32,7 @@ import com.metallic.chiaki.session.* import com.metallic.chiaki.touchcontrols.TouchpadOnlyFragment import com.metallic.chiaki.touchcontrols.TouchControlsFragment import kotlinx.android.synthetic.main.activity_stream.* +import kotlin.math.min private sealed class DialogContents private object StreamQuitDialog: DialogContents() @@ -105,6 +108,19 @@ class StreamActivity : AppCompatActivity(), View.OnSystemUiVisibilityChangeListe textureView.addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ -> adjustTextureViewAspect() } + + if(Preferences(this).rumbleEnabled) + { + val vibrator = getSystemService(VIBRATOR_SERVICE) as Vibrator + viewModel.session.rumbleState.observe(this, Observer { + val amplitude = min(255, (it.left.toInt() + it.right.toInt()) / 2) + vibrator.cancel() + if(amplitude == 0) + return@Observer + if(android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) + vibrator.vibrate(VibrationEffect.createOneShot(1000, amplitude)) + }) + } } override fun onAttachFragment(fragment: Fragment) diff --git a/android/app/src/main/res/drawable/ic_rumble.xml b/android/app/src/main/res/drawable/ic_rumble.xml new file mode 100644 index 0000000..4ecdb61 --- /dev/null +++ b/android/app/src/main/res/drawable/ic_rumble.xml @@ -0,0 +1,9 @@ + + + diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml index 0f54522..0f7161d 100644 --- a/android/app/src/main/res/values/strings.xml +++ b/android/app/src/main/res/values/strings.xml @@ -88,6 +88,8 @@ H265 (PS5 only) Swap Cross/Moon and Box/Pyramid Buttons Swap face buttons if default mapping is incorrect (e.g. for 8BitDo controllers) + Rumble + Use phone vibration motor for rumble Are you sure you want to delete the registered console %s with ID %s? Are you sure you want to delete the console entry for %s? Keep @@ -101,7 +103,8 @@ discovery_enabled on_screen_controls_enabled - touchpad_only_enabled + touchpad_only_enabled + rumble_enabled log_verbose import_settings export_settings diff --git a/android/app/src/main/res/xml/preferences.xml b/android/app/src/main/res/xml/preferences.xml index 0416f6e..e87afdb 100644 --- a/android/app/src/main/res/xml/preferences.xml +++ b/android/app/src/main/res/xml/preferences.xml @@ -19,6 +19,12 @@ app:summary="@string/preferences_swap_cross_moon_summary" app:icon="@drawable/ic_gamepad" /> + + Date: Wed, 13 Jan 2021 15:23:03 +0100 Subject: [PATCH 175/237] Add Rumble Fallback for Pre-O Android --- .../java/com/metallic/chiaki/stream/StreamActivity.kt | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/android/app/src/main/java/com/metallic/chiaki/stream/StreamActivity.kt b/android/app/src/main/java/com/metallic/chiaki/stream/StreamActivity.kt index d94fa0d..a25772d 100644 --- a/android/app/src/main/java/com/metallic/chiaki/stream/StreamActivity.kt +++ b/android/app/src/main/java/com/metallic/chiaki/stream/StreamActivity.kt @@ -5,12 +5,8 @@ package com.metallic.chiaki.stream import android.animation.Animator import android.animation.AnimatorListenerAdapter import android.app.AlertDialog -import android.content.res.Configuration import android.graphics.Matrix -import android.os.Bundle -import android.os.Handler -import android.os.VibrationEffect -import android.os.Vibrator +import android.os.* import android.view.KeyEvent import android.view.MotionEvent import android.view.TextureView @@ -23,7 +19,6 @@ import androidx.fragment.app.Fragment import androidx.lifecycle.* import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.metallic.chiaki.R -import com.metallic.chiaki.common.LogManager import com.metallic.chiaki.common.Preferences import com.metallic.chiaki.common.ext.viewModelFactory import com.metallic.chiaki.lib.ConnectInfo @@ -117,8 +112,10 @@ class StreamActivity : AppCompatActivity(), View.OnSystemUiVisibilityChangeListe vibrator.cancel() if(amplitude == 0) return@Observer - if(android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) vibrator.vibrate(VibrationEffect.createOneShot(1000, amplitude)) + else + vibrator.vibrate(1000) }) } } From c1a4504470d1f0b0481364629edbffb6e11df01a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Wed, 13 Jan 2021 17:14:07 +0100 Subject: [PATCH 176/237] Add L3 and R3 to Android Touch Controls --- .../touchcontrols/TouchControlsFragment.kt | 2 + .../main/res/drawable/control_button_l3.xml | 12 +++++ .../drawable/control_button_l3_pressed.xml | 12 +++++ .../main/res/drawable/control_button_r3.xml | 12 +++++ .../drawable/control_button_r3_pressed.xml | 12 +++++ .../src/main/res/layout/fragment_controls.xml | 21 +++++++++ assets/controls/l3.svg | 36 +++++++++++++++ assets/controls/l3_raw.svg | 44 +++++++++++++++++++ assets/controls/r3.svg | 36 +++++++++++++++ assets/controls/r3_raw.svg | 44 +++++++++++++++++++ 10 files changed, 231 insertions(+) create mode 100644 android/app/src/main/res/drawable/control_button_l3.xml create mode 100644 android/app/src/main/res/drawable/control_button_l3_pressed.xml create mode 100644 android/app/src/main/res/drawable/control_button_r3.xml create mode 100644 android/app/src/main/res/drawable/control_button_r3_pressed.xml create mode 100644 assets/controls/l3.svg create mode 100644 assets/controls/l3_raw.svg create mode 100644 assets/controls/r3.svg create mode 100644 assets/controls/r3_raw.svg diff --git a/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchControlsFragment.kt b/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchControlsFragment.kt index 9c4ca51..644f5f3 100644 --- a/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchControlsFragment.kt +++ b/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchControlsFragment.kt @@ -41,6 +41,8 @@ class TouchControlsFragment : Fragment() boxButtonView.buttonPressedCallback = buttonStateChanged(ControllerState.BUTTON_BOX) l1ButtonView.buttonPressedCallback = buttonStateChanged(ControllerState.BUTTON_L1) r1ButtonView.buttonPressedCallback = buttonStateChanged(ControllerState.BUTTON_R1) + l3ButtonView.buttonPressedCallback = buttonStateChanged(ControllerState.BUTTON_L3) + r3ButtonView.buttonPressedCallback = buttonStateChanged(ControllerState.BUTTON_R3) optionsButtonView.buttonPressedCallback = buttonStateChanged(ControllerState.BUTTON_OPTIONS) shareButtonView.buttonPressedCallback = buttonStateChanged(ControllerState.BUTTON_SHARE) psButtonView.buttonPressedCallback = buttonStateChanged(ControllerState.BUTTON_PS) diff --git a/android/app/src/main/res/drawable/control_button_l3.xml b/android/app/src/main/res/drawable/control_button_l3.xml new file mode 100644 index 0000000..34c3382 --- /dev/null +++ b/android/app/src/main/res/drawable/control_button_l3.xml @@ -0,0 +1,12 @@ + + + diff --git a/android/app/src/main/res/drawable/control_button_l3_pressed.xml b/android/app/src/main/res/drawable/control_button_l3_pressed.xml new file mode 100644 index 0000000..43f239a --- /dev/null +++ b/android/app/src/main/res/drawable/control_button_l3_pressed.xml @@ -0,0 +1,12 @@ + + + diff --git a/android/app/src/main/res/drawable/control_button_r3.xml b/android/app/src/main/res/drawable/control_button_r3.xml new file mode 100644 index 0000000..0f2bcfd --- /dev/null +++ b/android/app/src/main/res/drawable/control_button_r3.xml @@ -0,0 +1,12 @@ + + + diff --git a/android/app/src/main/res/drawable/control_button_r3_pressed.xml b/android/app/src/main/res/drawable/control_button_r3_pressed.xml new file mode 100644 index 0000000..08635e8 --- /dev/null +++ b/android/app/src/main/res/drawable/control_button_r3_pressed.xml @@ -0,0 +1,12 @@ + + + diff --git a/android/app/src/main/res/layout/fragment_controls.xml b/android/app/src/main/res/layout/fragment_controls.xml index a2c24df..0bf8011 100644 --- a/android/app/src/main/res/layout/fragment_controls.xml +++ b/android/app/src/main/res/layout/fragment_controls.xml @@ -112,6 +112,27 @@ + + + + + + + + + + + image/svg+xml + + + + + + + + + diff --git a/assets/controls/l3_raw.svg b/assets/controls/l3_raw.svg new file mode 100644 index 0000000..f04135f --- /dev/null +++ b/assets/controls/l3_raw.svg @@ -0,0 +1,44 @@ + + + + + + + image/svg+xml + + + + + + + + L3 + + diff --git a/assets/controls/r3.svg b/assets/controls/r3.svg new file mode 100644 index 0000000..b5f1fa7 --- /dev/null +++ b/assets/controls/r3.svg @@ -0,0 +1,36 @@ + + + + + + + image/svg+xml + + + + + + + + + diff --git a/assets/controls/r3_raw.svg b/assets/controls/r3_raw.svg new file mode 100644 index 0000000..5f47c8e --- /dev/null +++ b/assets/controls/r3_raw.svg @@ -0,0 +1,44 @@ + + + + + + + image/svg+xml + + + + + + + + R3 + + From b69bf280f8656df483c7f2680d52168f5cdc8c7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Wed, 13 Jan 2021 19:33:54 +0100 Subject: [PATCH 177/237] Extend Face Button Touch Areas on Android --- .../chiaki/touchcontrols/ButtonView.kt | 27 ++++++++- .../metallic/chiaki/touchcontrols/Vector.kt | 7 +++ .../src/main/res/layout/fragment_controls.xml | 56 ++++++++++--------- android/app/src/main/res/values/dimens.xml | 6 +- 4 files changed, 67 insertions(+), 29 deletions(-) diff --git a/android/app/src/main/java/com/metallic/chiaki/touchcontrols/ButtonView.kt b/android/app/src/main/java/com/metallic/chiaki/touchcontrols/ButtonView.kt index b2eba6b..e82ffac 100644 --- a/android/app/src/main/java/com/metallic/chiaki/touchcontrols/ButtonView.kt +++ b/android/app/src/main/java/com/metallic/chiaki/touchcontrols/ButtonView.kt @@ -8,6 +8,8 @@ import android.graphics.drawable.Drawable import android.util.AttributeSet import android.view.MotionEvent import android.view.View +import android.view.ViewGroup +import androidx.core.view.children import com.metallic.chiaki.R class ButtonView @JvmOverloads constructor( @@ -46,15 +48,36 @@ class ButtonView @JvmOverloads constructor( { super.onDraw(canvas) val drawable = if(buttonPressed) drawablePressed else drawableIdle - drawable?.setBounds(0, 0, width, height) + drawable?.setBounds(paddingLeft, paddingTop, width - paddingRight, height - paddingBottom) drawable?.draw(canvas) } + /** + * If this button overlaps with others in the same layout, + * let the one whose center is closest to the touch handle it. + */ + private fun bestFittingTouchView(x: Float, y: Float): View + { + val loc = locationOnScreen + Vector(x, y) + return (parent as? ViewGroup)?.children?.filter { + it is ButtonView + }?.filter { + val pos = it.locationOnScreen + loc.x >= pos.x && loc.x < pos.x + it.width && loc.y >= pos.y && loc.y < pos.y + it.height + }?.sortedBy { + (loc - (it.locationOnScreen + Vector(it.width.toFloat(), it.height.toFloat()) * 0.5f)).lengthSq + }?.firstOrNull() ?: this + } + override fun onTouchEvent(event: MotionEvent): Boolean { when(event.action) { - MotionEvent.ACTION_DOWN -> buttonPressed = true + MotionEvent.ACTION_DOWN -> { + if(bestFittingTouchView(event.x, event.y) != this) + return false + buttonPressed = true + } MotionEvent.ACTION_UP -> buttonPressed = false } return true diff --git a/android/app/src/main/java/com/metallic/chiaki/touchcontrols/Vector.kt b/android/app/src/main/java/com/metallic/chiaki/touchcontrols/Vector.kt index 36216e8..208c41a 100644 --- a/android/app/src/main/java/com/metallic/chiaki/touchcontrols/Vector.kt +++ b/android/app/src/main/java/com/metallic/chiaki/touchcontrols/Vector.kt @@ -2,6 +2,7 @@ package com.metallic.chiaki.touchcontrols +import android.view.View import kotlin.math.sqrt data class Vector(val x: Float, val y: Float) @@ -18,4 +19,10 @@ data class Vector(val x: Float, val y: Float) val lengthSq get() = x*x + y*y val length get() = sqrt(lengthSq) val normalized get() = this / length +} + +val View.locationOnScreen: Vector get() { + val v = intArrayOf(0, 0) + this.getLocationOnScreen(v) + return Vector(v[0].toFloat(), v[1].toFloat()) } \ No newline at end of file diff --git a/android/app/src/main/res/layout/fragment_controls.xml b/android/app/src/main/res/layout/fragment_controls.xml index 0bf8011..3082f35 100644 --- a/android/app/src/main/res/layout/fragment_controls.xml +++ b/android/app/src/main/res/layout/fragment_controls.xml @@ -1,10 +1,12 @@ + android:clipChildren="false" + tools:ignore="RtlHardcoded,RtlSymmetry"> + app:layout_constraintBottom_toBottomOf="parent" /> @@ -116,7 +130,6 @@ android:id="@+id/l3ButtonView" android:layout_width="64dp" android:layout_height="64dp" - android:padding="8dp" app:drawableIdle="@drawable/control_button_l3" app:drawablePressed="@drawable/control_button_l3_pressed" app:layout_constraintLeft_toLeftOf="parent" @@ -126,7 +139,6 @@ android:id="@+id/r3ButtonView" android:layout_width="64dp" android:layout_height="64dp" - android:padding="8dp" app:drawableIdle="@drawable/control_button_r3" app:drawablePressed="@drawable/control_button_r3_pressed" app:layout_constraintRight_toRightOf="parent" @@ -137,7 +149,6 @@ android:id="@+id/psButtonView" android:layout_width="32dp" android:layout_height="32dp" - android:padding="8dp" android:layout_marginBottom="8dp" app:drawableIdle="@drawable/control_button_home" app:drawablePressed="@drawable/control_button_home_pressed" @@ -150,7 +161,6 @@ android:id="@+id/touchpadButtonView" android:layout_width="32dp" android:layout_height="32dp" - android:padding="8dp" android:layout_marginTop="8dp" app:drawableIdle="@drawable/control_button_touchpad" app:drawablePressed="@drawable/control_button_touchpad_pressed" @@ -162,7 +172,6 @@ android:id="@+id/l2ButtonView" android:layout_width="64dp" android:layout_height="32dp" - android:padding="8dp" android:layout_marginTop="8dp" android:layout_marginLeft="8dp" app:drawableIdle="@drawable/control_button_l2" @@ -174,7 +183,6 @@ android:id="@+id/l1ButtonView" android:layout_width="64dp" android:layout_height="32dp" - android:padding="8dp" android:layout_marginTop="8dp" android:layout_marginLeft="8dp" app:drawableIdle="@drawable/control_button_l1" @@ -186,7 +194,6 @@ android:id="@+id/shareButtonView" android:layout_width="32dp" android:layout_height="32dp" - android:padding="8dp" android:layout_marginTop="8dp" android:layout_marginLeft="8dp" app:drawableIdle="@drawable/control_button_share" @@ -198,7 +205,6 @@ android:id="@+id/r2ButtonView" android:layout_width="64dp" android:layout_height="32dp" - android:padding="8dp" android:layout_marginTop="8dp" android:layout_marginRight="8dp" app:drawableIdle="@drawable/control_button_r2" @@ -210,7 +216,6 @@ android:id="@+id/r1ButtonView" android:layout_width="64dp" android:layout_height="32dp" - android:padding="8dp" android:layout_marginTop="8dp" android:layout_marginRight="8dp" app:drawableIdle="@drawable/control_button_r1" @@ -222,7 +227,6 @@ android:id="@+id/optionsButtonView" android:layout_width="32dp" android:layout_height="32dp" - android:padding="8dp" android:layout_marginTop="8dp" android:layout_marginRight="8dp" app:drawableIdle="@drawable/control_button_options" diff --git a/android/app/src/main/res/values/dimens.xml b/android/app/src/main/res/values/dimens.xml index 543f0f4..9ee2e9b 100644 --- a/android/app/src/main/res/values/dimens.xml +++ b/android/app/src/main/res/values/dimens.xml @@ -1,6 +1,10 @@ - 48dp + 88dp + 176dp + 24dp + 16dp + 64dp 48dp 32dp 48dp From 3a90ef0a658eac2506a17d87b55e084337814765 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Wed, 13 Jan 2021 20:24:25 +0100 Subject: [PATCH 178/237] Extend DPad Touch Area on Android --- .../java/com/metallic/chiaki/touchcontrols/DPadView.kt | 2 +- android/app/src/main/res/layout/fragment_controls.xml | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/android/app/src/main/java/com/metallic/chiaki/touchcontrols/DPadView.kt b/android/app/src/main/java/com/metallic/chiaki/touchcontrols/DPadView.kt index 9386e8f..0006546 100644 --- a/android/app/src/main/java/com/metallic/chiaki/touchcontrols/DPadView.kt +++ b/android/app/src/main/java/com/metallic/chiaki/touchcontrols/DPadView.kt @@ -71,7 +71,7 @@ class DPadView @JvmOverloads constructor( else drawable = dpadIdleDrawable - drawable?.setBounds(0, 0, width, height) + drawable?.setBounds(paddingLeft, paddingTop, width - paddingRight, height - paddingBottom) //drawable?.alpha = 127 drawable?.draw(canvas) } diff --git a/android/app/src/main/res/layout/fragment_controls.xml b/android/app/src/main/res/layout/fragment_controls.xml index 3082f35..7d4842f 100644 --- a/android/app/src/main/res/layout/fragment_controls.xml +++ b/android/app/src/main/res/layout/fragment_controls.xml @@ -49,10 +49,11 @@ From 510064c8996d1e2bdc69053bc85dde1b999989ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Thu, 14 Jan 2021 18:56:42 +0100 Subject: [PATCH 179/237] Use SurfaceView on Android --- android/app/src/main/cpp/video-decoder.c | 55 +++++---- .../java/com/metallic/chiaki/lib/Chiaki.kt | 4 +- .../metallic/chiaki/session/StreamSession.kt | 30 ++++- .../chiaki/stream/AspectRatioFrameLayout.kt | 68 +++++++++++ .../metallic/chiaki/stream/StreamActivity.kt | 110 ++++++++++-------- .../src/main/res/layout/activity_stream.xml | 19 ++- 6 files changed, 206 insertions(+), 80 deletions(-) create mode 100644 android/app/src/main/java/com/metallic/chiaki/stream/AspectRatioFrameLayout.kt diff --git a/android/app/src/main/cpp/video-decoder.c b/android/app/src/main/cpp/video-decoder.c index d57623d..1146ad8 100644 --- a/android/app/src/main/cpp/video-decoder.c +++ b/android/app/src/main/cpp/video-decoder.c @@ -26,29 +26,34 @@ ChiakiErrorCode android_chiaki_video_decoder_init(AndroidChiakiVideoDecoder *dec return chiaki_mutex_init(&decoder->codec_mutex, false); } +static void kill_decoder(AndroidChiakiVideoDecoder *decoder) +{ + chiaki_mutex_lock(&decoder->codec_mutex); + decoder->shutdown_output = true; + ssize_t codec_buf_index = AMediaCodec_dequeueInputBuffer(decoder->codec, 1000); + if(codec_buf_index >= 0) + { + CHIAKI_LOGI(decoder->log, "Video Decoder sending EOS buffer"); + AMediaCodec_queueInputBuffer(decoder->codec, (size_t)codec_buf_index, 0, 0, decoder->timestamp_cur++, AMEDIACODEC_BUFFER_FLAG_END_OF_STREAM); + AMediaCodec_stop(decoder->codec); + chiaki_mutex_unlock(&decoder->codec_mutex); + chiaki_thread_join(&decoder->output_thread, NULL); + } + else + { + CHIAKI_LOGE(decoder->log, "Failed to get input buffer for shutting down Video Decoder!"); + AMediaCodec_stop(decoder->codec); + chiaki_mutex_unlock(&decoder->codec_mutex); + } + AMediaCodec_delete(decoder->codec); + decoder->codec = NULL; + decoder->shutdown_output = false; +} + void android_chiaki_video_decoder_fini(AndroidChiakiVideoDecoder *decoder) { if(decoder->codec) - { - chiaki_mutex_lock(&decoder->codec_mutex); - decoder->shutdown_output = true; - ssize_t codec_buf_index = AMediaCodec_dequeueInputBuffer(decoder->codec, -1); - if(codec_buf_index >= 0) - { - CHIAKI_LOGI(decoder->log, "Video Decoder sending EOS buffer"); - AMediaCodec_queueInputBuffer(decoder->codec, (size_t)codec_buf_index, 0, 0, decoder->timestamp_cur++, AMEDIACODEC_BUFFER_FLAG_END_OF_STREAM); - AMediaCodec_stop(decoder->codec); - chiaki_mutex_unlock(&decoder->codec_mutex); - chiaki_thread_join(&decoder->output_thread, NULL); - } - else - { - CHIAKI_LOGE(decoder->log, "Failed to get input buffer for shutting down Video Decoder!"); - AMediaCodec_stop(decoder->codec); - chiaki_mutex_unlock(&decoder->codec_mutex); - } - AMediaCodec_delete(decoder->codec); - } + kill_decoder(decoder); chiaki_mutex_fini(&decoder->codec_mutex); } @@ -56,6 +61,16 @@ void android_chiaki_video_decoder_set_surface(AndroidChiakiVideoDecoder *decoder { chiaki_mutex_lock(&decoder->codec_mutex); + if(!surface) + { + if(decoder->codec) + { + kill_decoder(decoder); + CHIAKI_LOGI(decoder->log, "Decoder shut down after surface was removed"); + } + return; + } + if(decoder->codec) { #if __ANDROID_API__ >= 23 diff --git a/android/app/src/main/java/com/metallic/chiaki/lib/Chiaki.kt b/android/app/src/main/java/com/metallic/chiaki/lib/Chiaki.kt index 023da90..b8d7204 100644 --- a/android/app/src/main/java/com/metallic/chiaki/lib/Chiaki.kt +++ b/android/app/src/main/java/com/metallic/chiaki/lib/Chiaki.kt @@ -91,7 +91,7 @@ private class ChiakiNative @JvmStatic external fun sessionStart(ptr: Long): Int @JvmStatic external fun sessionStop(ptr: Long): Int @JvmStatic external fun sessionJoin(ptr: Long): Int - @JvmStatic external fun sessionSetSurface(ptr: Long, surface: Surface) + @JvmStatic external fun sessionSetSurface(ptr: Long, surface: Surface?) @JvmStatic external fun sessionSetControllerState(ptr: Long, controllerState: ControllerState) @JvmStatic external fun sessionSetLoginPin(ptr: Long, pin: String) @JvmStatic external fun discoveryServiceCreate(result: CreateResult, options: DiscoveryServiceOptions, javaService: DiscoveryService) @@ -350,7 +350,7 @@ class Session(connectInfo: ConnectInfo, logFile: String?, logVerbose: Boolean) event(RumbleEvent(left.toUByte(), right.toUByte())) } - fun setSurface(surface: Surface) + fun setSurface(surface: Surface?) { ChiakiNative.sessionSetSurface(nativePtr, surface) } diff --git a/android/app/src/main/java/com/metallic/chiaki/session/StreamSession.kt b/android/app/src/main/java/com/metallic/chiaki/session/StreamSession.kt index d7f127f..dd32a40 100644 --- a/android/app/src/main/java/com/metallic/chiaki/session/StreamSession.kt +++ b/android/app/src/main/java/com/metallic/chiaki/session/StreamSession.kt @@ -28,7 +28,8 @@ class StreamSession(val connectInfo: ConnectInfo, val logManager: LogManager, va private val _rumbleState = MutableLiveData(RumbleEvent(0U, 0U)) val rumbleState: LiveData get() = _rumbleState - var surfaceTexture: SurfaceTexture? = null + private var surfaceTexture: SurfaceTexture? = null + private var surface: Surface? = null init { @@ -61,9 +62,9 @@ class StreamSession(val connectInfo: ConnectInfo, val logManager: LogManager, va _state.value = StreamStateConnecting session.eventCallback = this::eventCallback session.start() - val surfaceTexture = surfaceTexture - if(surfaceTexture != null) - session.setSurface(Surface(surfaceTexture)) + val surface = surface + if(surface != null) + session.setSurface(surface) this.session = session } catch(e: CreateError) @@ -92,6 +93,26 @@ class StreamSession(val connectInfo: ConnectInfo, val logManager: LogManager, va } } + fun attachToSurfaceView(surfaceView: SurfaceView) + { + surfaceView.holder.addCallback(object: SurfaceHolder.Callback { + override fun surfaceCreated(holder: SurfaceHolder) { } + + override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) + { + val surface = holder.surface + this@StreamSession.surface = surface + session?.setSurface(surface) + } + + override fun surfaceDestroyed(holder: SurfaceHolder) + { + this@StreamSession.surface = null + session?.setSurface(null) + } + }) + } + fun attachToTextureView(textureView: TextureView) { textureView.surfaceTextureListener = object: TextureView.SurfaceTextureListener { @@ -100,6 +121,7 @@ class StreamSession(val connectInfo: ConnectInfo, val logManager: LogManager, va if(surfaceTexture != null) return surfaceTexture = surface + this@StreamSession.surface = Surface(surfaceTexture) session?.setSurface(Surface(surface)) } diff --git a/android/app/src/main/java/com/metallic/chiaki/stream/AspectRatioFrameLayout.kt b/android/app/src/main/java/com/metallic/chiaki/stream/AspectRatioFrameLayout.kt new file mode 100644 index 0000000..5394f02 --- /dev/null +++ b/android/app/src/main/java/com/metallic/chiaki/stream/AspectRatioFrameLayout.kt @@ -0,0 +1,68 @@ +package com.metallic.chiaki.stream + +import android.content.Context +import android.util.AttributeSet +import android.widget.FrameLayout + +// see ExoPlayer's AspectRatioFrameLayout +class AspectRatioFrameLayout @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null): FrameLayout(context, attrs) +{ + companion object + { + private const val MAX_ASPECT_RATIO_DEFORMATION_FRACTION = 0.01f + } + + var aspectRatio = 0f + set(value) + { + if(field != value) + { + field = value + requestLayout() + } + } + + var mode: TransformMode = TransformMode.FIT + set(value) + { + if(field != value) + { + field = value + requestLayout() + } + } + + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) + { + super.onMeasure(widthMeasureSpec, heightMeasureSpec) + if(aspectRatio <= 0) + { + // Aspect ratio not set. + return + } + var width = measuredWidth + var height = measuredHeight + val viewAspectRatio = width.toFloat() / height + val aspectDeformation = aspectRatio / viewAspectRatio - 1 + if(Math.abs(aspectDeformation) <= MAX_ASPECT_RATIO_DEFORMATION_FRACTION) + return + when(mode) + { + TransformMode.ZOOM -> + if(aspectDeformation > 0) + width = (height * aspectRatio).toInt() + else + height = (width / aspectRatio).toInt() + TransformMode.FIT -> + if(aspectDeformation > 0) + height = (width / aspectRatio).toInt() + else + width = (height * aspectRatio).toInt() + TransformMode.STRETCH -> {} + } + super.onMeasure( + MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY) + ) + } +} diff --git a/android/app/src/main/java/com/metallic/chiaki/stream/StreamActivity.kt b/android/app/src/main/java/com/metallic/chiaki/stream/StreamActivity.kt index a25772d..32b9fd3 100644 --- a/android/app/src/main/java/com/metallic/chiaki/stream/StreamActivity.kt +++ b/android/app/src/main/java/com/metallic/chiaki/stream/StreamActivity.kt @@ -7,12 +7,12 @@ import android.animation.AnimatorListenerAdapter import android.app.AlertDialog import android.graphics.Matrix import android.os.* -import android.view.KeyEvent -import android.view.MotionEvent -import android.view.TextureView -import android.view.View +import android.transition.TransitionManager +import android.view.* import android.widget.EditText import androidx.appcompat.app.AppCompatActivity +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.constraintlayout.widget.ConstraintSet import androidx.core.view.isGone import androidx.core.view.isVisible import androidx.fragment.app.Fragment @@ -44,6 +44,7 @@ class StreamActivity : AppCompatActivity(), View.OnSystemUiVisibilityChangeListe private lateinit var viewModel: StreamViewModel private val uiVisibilityHandler = Handler() + private val streamView: View get() = surfaceView override fun onCreate(savedInstanceState: Bundle?) { @@ -94,15 +95,17 @@ class StreamActivity : AppCompatActivity(), View.OnSystemUiVisibilityChangeListe if (displayModeToggle.checkedButtonId == -1) displayModeToggle.check(checkedId) - adjustTextureViewAspect() + adjustStreamViewAspect() showOverlay() } - viewModel.session.attachToTextureView(textureView) + //viewModel.session.attachToTextureView(textureView) + viewModel.session.attachToSurfaceView(surfaceView) viewModel.session.state.observe(this, Observer { this.stateChanged(it) }) - textureView.addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ -> - adjustTextureViewAspect() - } + /*streamView.addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ -> + adjustStreamViewAspect() + }*/ + adjustStreamViewAspect() if(Preferences(this).rumbleEnabled) { @@ -306,73 +309,84 @@ class StreamActivity : AppCompatActivity(), View.OnSystemUiVisibilityChangeListe } } - private fun adjustTextureViewAspect() + private fun adjustTextureViewAspect(textureView: TextureView) { - val displayInfo = DisplayInfo(viewModel.session.connectInfo.videoProfile, textureView) - val resolution = displayInfo.computeResolutionFor(displayModeToggle.checkedButtonId) - + val trans = TextureViewTransform(viewModel.session.connectInfo.videoProfile, textureView) + val resolution = trans.resolutionFor(TransformMode.fromButton(displayModeToggle.checkedButtonId)) Matrix().also { textureView.getTransform(it) - it.setScale(resolution.width / displayInfo.viewWidth, resolution.height / displayInfo.viewHeight) - it.postTranslate((displayInfo.viewWidth - resolution.width) * 0.5f, (displayInfo.viewHeight - resolution.height) * 0.5f) + it.setScale(resolution.width / trans.viewWidth, resolution.height / trans.viewHeight) + it.postTranslate((trans.viewWidth - resolution.width) * 0.5f, (trans.viewHeight - resolution.height) * 0.5f) textureView.setTransform(it) } } + private fun adjustSurfaceViewAspect() + { + val videoProfile = viewModel.session.connectInfo.videoProfile + aspectRatioLayout.aspectRatio = videoProfile.width.toFloat() / videoProfile.height.toFloat() + aspectRatioLayout.mode = TransformMode.fromButton(displayModeToggle.checkedButtonId) + } + + private fun adjustStreamViewAspect() = adjustSurfaceViewAspect() + override fun dispatchKeyEvent(event: KeyEvent) = viewModel.input.dispatchKeyEvent(event) || super.dispatchKeyEvent(event) override fun onGenericMotionEvent(event: MotionEvent) = viewModel.input.onGenericMotionEvent(event) || super.onGenericMotionEvent(event) } - -class DisplayInfo constructor(val videoProfile: ConnectVideoProfile, val textureView: TextureView) +enum class TransformMode { - val contentWidth : Float get() = videoProfile.width.toFloat() - val contentHeight : Float get() = videoProfile.height.toFloat() + FIT, + STRETCH, + ZOOM; + + companion object + { + fun fromButton(displayModeButtonId: Int) + = when (displayModeButtonId) + { + R.id.display_mode_stretch_button -> STRETCH + R.id.display_mode_zoom_button -> ZOOM + else -> FIT + } + } +} + +class TextureViewTransform(private val videoProfile: ConnectVideoProfile, private val textureView: TextureView) +{ + private val contentWidth : Float get() = videoProfile.width.toFloat() + private val contentHeight : Float get() = videoProfile.height.toFloat() val viewWidth : Float get() = textureView.width.toFloat() val viewHeight : Float get() = textureView.height.toFloat() - val contentAspect : Float get() = contentHeight / contentWidth + private val contentAspect : Float get() = contentHeight / contentWidth - fun computeResolutionFor(displayModeButtonId: Int) : Resolution - { - when (displayModeButtonId) + fun resolutionFor(mode: TransformMode): Resolution + = when(mode) { - R.id.display_mode_stretch_button -> return computeStrechedResolution() - R.id.display_mode_zoom_button -> return computeZoomedResolution() - else -> return computeNormalResolution() + TransformMode.STRETCH -> strechedResolution + TransformMode.ZOOM -> zoomedResolution + TransformMode.FIT -> normalResolution } - } - private fun computeStrechedResolution(): Resolution - { - return Resolution(viewWidth, viewHeight) - } + private val strechedResolution get() = Resolution(viewWidth, viewHeight) - private fun computeZoomedResolution(): Resolution - { - if (viewHeight > viewWidth * contentAspect) + private val zoomedResolution get() = + if(viewHeight > viewWidth * contentAspect) { val zoomFactor = viewHeight / contentHeight - return Resolution(contentWidth * zoomFactor, viewHeight) + Resolution(contentWidth * zoomFactor, viewHeight) } else { val zoomFactor = viewWidth / contentWidth - return Resolution(viewWidth, contentHeight * zoomFactor) + Resolution(viewWidth, contentHeight * zoomFactor) } - } - private fun computeNormalResolution(): Resolution - { - if (viewHeight > viewWidth * contentAspect) - { - return Resolution(viewWidth, viewWidth * contentAspect) - } + private val normalResolution get() = + if(viewHeight > viewWidth * contentAspect) + Resolution(viewWidth, viewWidth * contentAspect) else - { - return Resolution(viewHeight / contentAspect, viewHeight) - } - } - + Resolution(viewHeight / contentAspect, viewHeight) } diff --git a/android/app/src/main/res/layout/activity_stream.xml b/android/app/src/main/res/layout/activity_stream.xml index 255ccde..9c944c3 100644 --- a/android/app/src/main/res/layout/activity_stream.xml +++ b/android/app/src/main/res/layout/activity_stream.xml @@ -1,16 +1,24 @@ - - + android:layout_height="match_parent" + android:layout_gravity="center"> + + Date: Thu, 14 Jan 2021 20:44:56 +0100 Subject: [PATCH 180/237] Add nicer LR Buttons to Android --- .../main/res/drawable/control_button_l1.xml | 9 +- .../drawable/control_button_l1_pressed.xml | 9 +- .../main/res/drawable/control_button_l2.xml | 9 +- .../drawable/control_button_l2_pressed.xml | 9 +- .../main/res/drawable/control_button_r1.xml | 9 +- .../drawable/control_button_r1_pressed.xml | 9 +- .../main/res/drawable/control_button_r2.xml | 9 +- .../drawable/control_button_r2_pressed.xml | 9 +- .../src/main/res/layout/fragment_controls.xml | 40 +++--- assets/controls/l1.svg | 56 ++------ assets/controls/l2.svg | 56 ++------ assets/controls/lr12.svg | 124 ++++++++++++++++++ assets/controls/r1.svg | 56 ++------ assets/controls/r2.svg | 55 ++------ 14 files changed, 208 insertions(+), 251 deletions(-) create mode 100644 assets/controls/lr12.svg diff --git a/android/app/src/main/res/drawable/control_button_l1.xml b/android/app/src/main/res/drawable/control_button_l1.xml index a709f25..17f9594 100644 --- a/android/app/src/main/res/drawable/control_button_l1.xml +++ b/android/app/src/main/res/drawable/control_button_l1.xml @@ -1,13 +1,12 @@ + android:viewportHeight="67.73334"> diff --git a/android/app/src/main/res/drawable/control_button_l1_pressed.xml b/android/app/src/main/res/drawable/control_button_l1_pressed.xml index b8bcce0..0267190 100644 --- a/android/app/src/main/res/drawable/control_button_l1_pressed.xml +++ b/android/app/src/main/res/drawable/control_button_l1_pressed.xml @@ -1,13 +1,12 @@ + android:viewportHeight="67.73334"> diff --git a/android/app/src/main/res/drawable/control_button_l2.xml b/android/app/src/main/res/drawable/control_button_l2.xml index 6867aa0..8e75f40 100644 --- a/android/app/src/main/res/drawable/control_button_l2.xml +++ b/android/app/src/main/res/drawable/control_button_l2.xml @@ -1,13 +1,12 @@ + android:viewportHeight="67.73334"> diff --git a/android/app/src/main/res/drawable/control_button_l2_pressed.xml b/android/app/src/main/res/drawable/control_button_l2_pressed.xml index e4df2de..b120891 100644 --- a/android/app/src/main/res/drawable/control_button_l2_pressed.xml +++ b/android/app/src/main/res/drawable/control_button_l2_pressed.xml @@ -1,13 +1,12 @@ + android:viewportHeight="67.73334"> diff --git a/android/app/src/main/res/drawable/control_button_r1.xml b/android/app/src/main/res/drawable/control_button_r1.xml index a0b3262..d614419 100644 --- a/android/app/src/main/res/drawable/control_button_r1.xml +++ b/android/app/src/main/res/drawable/control_button_r1.xml @@ -1,13 +1,12 @@ + android:viewportHeight="67.73334"> diff --git a/android/app/src/main/res/drawable/control_button_r1_pressed.xml b/android/app/src/main/res/drawable/control_button_r1_pressed.xml index 5cadcb6..8d51983 100644 --- a/android/app/src/main/res/drawable/control_button_r1_pressed.xml +++ b/android/app/src/main/res/drawable/control_button_r1_pressed.xml @@ -1,13 +1,12 @@ + android:viewportHeight="67.73334"> diff --git a/android/app/src/main/res/drawable/control_button_r2.xml b/android/app/src/main/res/drawable/control_button_r2.xml index c6a625d..dd16994 100644 --- a/android/app/src/main/res/drawable/control_button_r2.xml +++ b/android/app/src/main/res/drawable/control_button_r2.xml @@ -1,13 +1,12 @@ + android:viewportHeight="67.73334"> diff --git a/android/app/src/main/res/drawable/control_button_r2_pressed.xml b/android/app/src/main/res/drawable/control_button_r2_pressed.xml index 260e60b..7cc84b1 100644 --- a/android/app/src/main/res/drawable/control_button_r2_pressed.xml +++ b/android/app/src/main/res/drawable/control_button_r2_pressed.xml @@ -1,13 +1,12 @@ + android:viewportHeight="67.73334"> diff --git a/android/app/src/main/res/layout/fragment_controls.xml b/android/app/src/main/res/layout/fragment_controls.xml index 7d4842f..3ca53c8 100644 --- a/android/app/src/main/res/layout/fragment_controls.xml +++ b/android/app/src/main/res/layout/fragment_controls.xml @@ -171,10 +171,9 @@ + app:layout_constraintTop_toTopOf="parent"/> + app:layout_constraintTop_toTopOf="parent"/> - @@ -49,28 +25,12 @@ - diff --git a/assets/controls/l2.svg b/assets/controls/l2.svg index 01e886f..4d561e0 100644 --- a/assets/controls/l2.svg +++ b/assets/controls/l2.svg @@ -5,37 +5,13 @@ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" - xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" - xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" - sodipodi:docname="l2.svg" - inkscape:version="1.0beta1 (5c3063637d, 2019-10-15)" id="svg8" version="1.1" - viewBox="0 0 67.733332 33.866668" - height="128" + viewBox="0 0 67.733332 67.733336" + height="256" width="256"> - @@ -49,28 +25,12 @@ - diff --git a/assets/controls/lr12.svg b/assets/controls/lr12.svg new file mode 100644 index 0000000..874475f --- /dev/null +++ b/assets/controls/lr12.svg @@ -0,0 +1,124 @@ + + + + + + + + image/svg+xml + + + + + + + + L1 + + + + R1 + + + + L2 + + + + R2 + + diff --git a/assets/controls/r1.svg b/assets/controls/r1.svg index ef51bc2..70f881c 100644 --- a/assets/controls/r1.svg +++ b/assets/controls/r1.svg @@ -5,37 +5,13 @@ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" - xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" - xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" - sodipodi:docname="r1.svg" - inkscape:version="1.0beta1 (5c3063637d, 2019-10-15)" id="svg8" version="1.1" - viewBox="0 0 67.733332 33.866668" - height="128" + viewBox="0 0 67.733332 67.733336" + height="256" width="256"> - @@ -49,28 +25,12 @@ - diff --git a/assets/controls/r2.svg b/assets/controls/r2.svg index 350ffaa..c32639d 100644 --- a/assets/controls/r2.svg +++ b/assets/controls/r2.svg @@ -5,37 +5,13 @@ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" - xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" - xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" - sodipodi:docname="r2.svg" - inkscape:version="1.0beta1 (5c3063637d, 2019-10-15)" id="svg8" version="1.1" - viewBox="0 0 67.733332 33.866668" - height="128" + viewBox="0 0 67.733332 67.733336" + height="256" width="256"> - @@ -49,28 +25,13 @@ - From 367489e2307b805c8bec0d072dfdd739a4549a15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Thu, 14 Jan 2021 20:50:38 +0100 Subject: [PATCH 181/237] Extend Touch Areas on Android --- .../src/main/res/layout/fragment_controls.xml | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/android/app/src/main/res/layout/fragment_controls.xml b/android/app/src/main/res/layout/fragment_controls.xml index 3ca53c8..94c2c52 100644 --- a/android/app/src/main/res/layout/fragment_controls.xml +++ b/android/app/src/main/res/layout/fragment_controls.xml @@ -148,9 +148,9 @@ Date: Fri, 15 Jan 2021 12:29:56 +0100 Subject: [PATCH 182/237] Update Android Dependencies --- android/app/build.gradle | 14 +++++++------- .../com/metallic/chiaki/stream/StreamActivity.kt | 12 +----------- .../app/src/main/res/layout/activity_stream.xml | 1 + android/build.gradle | 4 ++-- android/gradle/wrapper/gradle-wrapper.properties | 4 ++-- 5 files changed, 13 insertions(+), 22 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index fde2388..2b2c248 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -94,19 +94,19 @@ androidExtensions { dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" - implementation 'androidx.appcompat:appcompat:1.1.0' - implementation 'androidx.core:core-ktx:1.2.0' - implementation 'androidx.constraintlayout:constraintlayout:1.1.3' + implementation 'androidx.appcompat:appcompat:1.2.0' + implementation 'androidx.core:core-ktx:1.3.2' + implementation 'androidx.constraintlayout:constraintlayout:2.0.4' implementation 'androidx.recyclerview:recyclerview:1.1.0' - implementation 'androidx.preference:preference:1.1.0' - implementation 'com.google.android.material:material:1.1.0-beta02' + implementation 'androidx.preference:preference:1.1.1' + implementation 'com.google.android.material:material:1.2.1' implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0' implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0' implementation 'androidx.lifecycle:lifecycle-reactivestreams:2.2.0' - implementation "io.reactivex.rxjava2:rxjava:2.2.12" + implementation "io.reactivex.rxjava2:rxjava:2.2.20" implementation "io.reactivex.rxjava2:rxkotlin:2.4.0" implementation 'io.reactivex.rxjava2:rxandroid:2.1.1' - def room_version = "2.2.4" + def room_version = "2.2.6" implementation "androidx.room:room-runtime:$room_version" kapt "androidx.room:room-compiler:$room_version" implementation "androidx.room:room-ktx:$room_version" diff --git a/android/app/src/main/java/com/metallic/chiaki/stream/StreamActivity.kt b/android/app/src/main/java/com/metallic/chiaki/stream/StreamActivity.kt index 32b9fd3..9779987 100644 --- a/android/app/src/main/java/com/metallic/chiaki/stream/StreamActivity.kt +++ b/android/app/src/main/java/com/metallic/chiaki/stream/StreamActivity.kt @@ -44,7 +44,6 @@ class StreamActivity : AppCompatActivity(), View.OnSystemUiVisibilityChangeListe private lateinit var viewModel: StreamViewModel private val uiVisibilityHandler = Handler() - private val streamView: View get() = surfaceView override fun onCreate(savedInstanceState: Bundle?) { @@ -88,13 +87,7 @@ class StreamActivity : AppCompatActivity(), View.OnSystemUiVisibilityChangeListe showOverlay() } - displayModeToggle.addOnButtonCheckedListener { _, checkedId, _ -> - // following 'if' is a workaround until selectionRequired for MaterialButtonToggleGroup - // comes out of alpha. - // See https://stackoverflow.com/questions/56164004/required-single-selection-on-materialbuttontogglegroup - if (displayModeToggle.checkedButtonId == -1) - displayModeToggle.check(checkedId) - + displayModeToggle.addOnButtonCheckedListener { _, _, _ -> adjustStreamViewAspect() showOverlay() } @@ -102,9 +95,6 @@ class StreamActivity : AppCompatActivity(), View.OnSystemUiVisibilityChangeListe //viewModel.session.attachToTextureView(textureView) viewModel.session.attachToSurfaceView(surfaceView) viewModel.session.state.observe(this, Observer { this.stateChanged(it) }) - /*streamView.addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ -> - adjustStreamViewAspect() - }*/ adjustStreamViewAspect() if(Preferences(this).rumbleEnabled) diff --git a/android/app/src/main/res/layout/activity_stream.xml b/android/app/src/main/res/layout/activity_stream.xml index 9c944c3..99eea1b 100644 --- a/android/app/src/main/res/layout/activity_stream.xml +++ b/android/app/src/main/res/layout/activity_stream.xml @@ -83,6 +83,7 @@ app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:singleSelection="true" + app:selectionRequired="true" app:checkedButton="@id/display_mode_normal_button"> Date: Fri, 15 Jan 2021 14:36:29 +0100 Subject: [PATCH 183/237] Replace Deprecated Android Kotlin Extensions and Others --- android/app/build.gradle | 11 ++-- .../java/com/metallic/chiaki/lib/Chiaki.kt | 3 +- .../main/DisplayHostRecyclerViewAdapter.kt | 11 ++-- .../com/metallic/chiaki/main/MainActivity.kt | 48 +++++++++-------- .../EditManualConsoleActivity.kt | 31 +++++------ .../metallic/chiaki/regist/RegistActivity.kt | 42 ++++++++------- .../chiaki/regist/RegistExecuteActivity.kt | 34 ++++++------ .../metallic/chiaki/session/StreamInput.kt | 3 +- .../chiaki/settings/SettingsActivity.kt | 13 +++-- .../chiaki/settings/SettingsFragment.kt | 1 - .../chiaki/settings/SettingsLogsAdapter.kt | 16 +++--- .../chiaki/settings/SettingsLogsFragment.kt | 20 ++++--- .../SettingsRegisteredHostsAdapter.kt | 16 +++--- .../SettingsRegisteredHostsFragment.kt | 27 ++++++---- .../metallic/chiaki/stream/StreamActivity.kt | 54 +++++++++---------- .../touchcontrols/TouchControlsFragment.kt | 52 +++++++++--------- .../touchcontrols/TouchpadOnlyFragment.kt | 17 ++++-- 17 files changed, 215 insertions(+), 184 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 2b2c248..48bf54c 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -1,6 +1,6 @@ apply plugin: 'com.android.application' apply plugin: 'kotlin-android' -apply plugin: 'kotlin-android-extensions' +apply plugin: 'kotlin-parcelize' apply plugin: 'kotlin-kapt' def rootCMakeLists = "../../CMakeLists.txt" @@ -38,6 +38,9 @@ android { } } } + buildFeatures { + viewBinding true + } compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 @@ -61,6 +64,7 @@ android { } } + Properties properties = new Properties() def propertiesFile = file("../local.properties") if (propertiesFile.exists()) { @@ -86,11 +90,6 @@ tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all { } } -androidExtensions { - // for @Parcelize - experimental = true -} - dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" diff --git a/android/app/src/main/java/com/metallic/chiaki/lib/Chiaki.kt b/android/app/src/main/java/com/metallic/chiaki/lib/Chiaki.kt index b8d7204..9533c7c 100644 --- a/android/app/src/main/java/com/metallic/chiaki/lib/Chiaki.kt +++ b/android/app/src/main/java/com/metallic/chiaki/lib/Chiaki.kt @@ -3,8 +3,7 @@ package com.metallic.chiaki.lib import android.os.Parcelable import android.util.Log import android.view.Surface -import kotlinx.android.parcel.IgnoredOnParcel -import kotlinx.android.parcel.Parcelize +import kotlinx.parcelize.Parcelize import java.lang.Exception import java.net.InetSocketAddress import kotlin.math.abs diff --git a/android/app/src/main/java/com/metallic/chiaki/main/DisplayHostRecyclerViewAdapter.kt b/android/app/src/main/java/com/metallic/chiaki/main/DisplayHostRecyclerViewAdapter.kt index 0bd844e..465a2a7 100644 --- a/android/app/src/main/java/com/metallic/chiaki/main/DisplayHostRecyclerViewAdapter.kt +++ b/android/app/src/main/java/com/metallic/chiaki/main/DisplayHostRecyclerViewAdapter.kt @@ -3,6 +3,7 @@ package com.metallic.chiaki.main import android.util.Log +import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.view.animation.AnimationUtils @@ -16,8 +17,8 @@ import com.metallic.chiaki.common.DiscoveredDisplayHost import com.metallic.chiaki.common.DisplayHost import com.metallic.chiaki.common.ManualDisplayHost import com.metallic.chiaki.common.ext.inflate +import com.metallic.chiaki.databinding.ItemDisplayHostBinding import com.metallic.chiaki.lib.DiscoveryHost -import kotlinx.android.synthetic.main.item_display_host.view.* class DisplayHostDiffCallback(val old: List, val new: List): DiffUtil.Callback() { @@ -42,10 +43,10 @@ class DisplayHostRecyclerViewAdapter( diff.dispatchUpdatesTo(this) } - class ViewHolder(itemView: View): RecyclerView.ViewHolder(itemView) + class ViewHolder(val binding: ItemDisplayHostBinding): RecyclerView.ViewHolder(binding.root) override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) - = ViewHolder(parent.inflate(R.layout.item_display_host)) + = ViewHolder(ItemDisplayHostBinding.inflate(LayoutInflater.from(parent.context), parent, false)) override fun getItemCount() = hosts.count() @@ -53,7 +54,7 @@ class DisplayHostRecyclerViewAdapter( { val context = holder.itemView.context val host = hosts[position] - holder.itemView.also { + holder.binding.also { it.nameTextView.text = host.name it.hostTextView.text = context.getString(R.string.display_host_host, host.host) val id = host.id @@ -87,7 +88,7 @@ class DisplayHostRecyclerViewAdapter( else -> R.drawable.ic_console } ) - it.setOnClickListener { clickCallback(host) } + it.root.setOnClickListener { clickCallback(host) } val canWakeup = host.registeredHost != null val canEditDelete = host is ManualDisplayHost diff --git a/android/app/src/main/java/com/metallic/chiaki/main/MainActivity.kt b/android/app/src/main/java/com/metallic/chiaki/main/MainActivity.kt index 4fd8178..5bf1f49 100644 --- a/android/app/src/main/java/com/metallic/chiaki/main/MainActivity.kt +++ b/android/app/src/main/java/com/metallic/chiaki/main/MainActivity.kt @@ -17,52 +17,54 @@ import com.metallic.chiaki.R import com.metallic.chiaki.common.* import com.metallic.chiaki.common.ext.putRevealExtra import com.metallic.chiaki.common.ext.viewModelFactory +import com.metallic.chiaki.databinding.ActivityMainBinding import com.metallic.chiaki.lib.ConnectInfo import com.metallic.chiaki.lib.DiscoveryHost import com.metallic.chiaki.manualconsole.EditManualConsoleActivity import com.metallic.chiaki.regist.RegistActivity import com.metallic.chiaki.settings.SettingsActivity import com.metallic.chiaki.stream.StreamActivity -import kotlinx.android.synthetic.main.activity_main.* class MainActivity : AppCompatActivity() { private lateinit var viewModel: MainViewModel + private lateinit var binding: ActivityMainBinding private var discoveryMenuItem: MenuItem? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setContentView(R.layout.activity_main) + binding = ActivityMainBinding.inflate(layoutInflater) + setContentView(binding.root) title = "" - setSupportActionBar(toolbar) + setSupportActionBar(binding.toolbar) - floatingActionButton.setOnClickListener { - expandFloatingActionButton(!floatingActionButton.isExpanded) + binding.floatingActionButton.setOnClickListener { + expandFloatingActionButton(!binding.floatingActionButton.isExpanded) } - floatingActionButtonDialBackground.setOnClickListener { + binding.floatingActionButtonDialBackground.setOnClickListener { expandFloatingActionButton(false) } - addManualButton.setOnClickListener { addManualConsole() } - addManualLabelButton.setOnClickListener { addManualConsole() } + binding.addManualButton.setOnClickListener { addManualConsole() } + binding.addManualLabelButton.setOnClickListener { addManualConsole() } - registerButton.setOnClickListener { showRegistration() } - registerLabelButton.setOnClickListener { showRegistration() } + binding.registerButton.setOnClickListener { showRegistration() } + binding.registerLabelButton.setOnClickListener { showRegistration() } viewModel = ViewModelProvider(this, viewModelFactory { MainViewModel(getDatabase(this), Preferences(this)) }) .get(MainViewModel::class.java) val recyclerViewAdapter = DisplayHostRecyclerViewAdapter(this::hostTriggered, this::wakeupHost, this::editHost, this::deleteHost) - hostsRecyclerView.adapter = recyclerViewAdapter - hostsRecyclerView.layoutManager = LinearLayoutManager(this) + binding.hostsRecyclerView.adapter = recyclerViewAdapter + binding.hostsRecyclerView.layoutManager = LinearLayoutManager(this) viewModel.displayHosts.observe(this, Observer { - val top = hostsRecyclerView.computeVerticalScrollOffset() == 0 + val top = binding.hostsRecyclerView.computeVerticalScrollOffset() == 0 recyclerViewAdapter.hosts = it if(top) - hostsRecyclerView.scrollToPosition(0) + binding.hostsRecyclerView.scrollToPosition(0) updateEmptyInfo() }) @@ -76,19 +78,19 @@ class MainActivity : AppCompatActivity() { if(viewModel.displayHosts.value?.isEmpty() ?: true) { - emptyInfoLayout.visibility = View.VISIBLE + binding.emptyInfoLayout.visibility = View.VISIBLE val discoveryActive = viewModel.discoveryActive.value ?: false - emptyInfoImageView.setImageResource(if(discoveryActive) R.drawable.ic_discover_on else R.drawable.ic_discover_off) - emptyInfoTextView.setText(if(discoveryActive) R.string.display_hosts_empty_discovery_on_info else R.string.display_hosts_empty_discovery_off_info) + binding.emptyInfoImageView.setImageResource(if(discoveryActive) R.drawable.ic_discover_on else R.drawable.ic_discover_off) + binding.emptyInfoTextView.setText(if(discoveryActive) R.string.display_hosts_empty_discovery_on_info else R.string.display_hosts_empty_discovery_off_info) } else - emptyInfoLayout.visibility = View.GONE + binding.emptyInfoLayout.visibility = View.GONE } private fun expandFloatingActionButton(expand: Boolean) { - floatingActionButton.isExpanded = expand - floatingActionButton.isActivated = floatingActionButton.isExpanded + binding.floatingActionButton.isExpanded = expand + binding.floatingActionButton.isActivated = binding.floatingActionButton.isExpanded } override fun onStart() @@ -105,7 +107,7 @@ class MainActivity : AppCompatActivity() override fun onBackPressed() { - if(floatingActionButton.isExpanded) + if(binding.floatingActionButton.isExpanded) { expandFloatingActionButton(false) return @@ -151,7 +153,7 @@ class MainActivity : AppCompatActivity() private fun addManualConsole() { Intent(this, EditManualConsoleActivity::class.java).also { - it.putRevealExtra(addManualButton, rootLayout) + it.putRevealExtra(binding.addManualButton, binding.rootLayout) startActivity(it, ActivityOptions.makeSceneTransitionAnimation(this).toBundle()) } } @@ -159,7 +161,7 @@ class MainActivity : AppCompatActivity() private fun showRegistration() { Intent(this, RegistActivity::class.java).also { - it.putRevealExtra(registerButton, rootLayout) + it.putRevealExtra(binding.registerButton, binding.rootLayout) startActivity(it, ActivityOptions.makeSceneTransitionAnimation(this).toBundle()) } } diff --git a/android/app/src/main/java/com/metallic/chiaki/manualconsole/EditManualConsoleActivity.kt b/android/app/src/main/java/com/metallic/chiaki/manualconsole/EditManualConsoleActivity.kt index 49e5f4d..bb64b87 100644 --- a/android/app/src/main/java/com/metallic/chiaki/manualconsole/EditManualConsoleActivity.kt +++ b/android/app/src/main/java/com/metallic/chiaki/manualconsole/EditManualConsoleActivity.kt @@ -16,10 +16,10 @@ import com.metallic.chiaki.common.RegisteredHost import com.metallic.chiaki.common.ext.RevealActivity import com.metallic.chiaki.common.ext.viewModelFactory import com.metallic.chiaki.common.getDatabase +import com.metallic.chiaki.databinding.ActivityEditManualBinding import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.disposables.CompositeDisposable import io.reactivex.rxkotlin.addTo -import kotlinx.android.synthetic.main.activity_edit_manual.* class EditManualConsoleActivity: AppCompatActivity(), RevealActivity { @@ -28,18 +28,20 @@ class EditManualConsoleActivity: AppCompatActivity(), RevealActivity const val EXTRA_MANUAL_HOST_ID = "manual_host_id" } - override val revealIntent: Intent get() = intent - override val revealRootLayout: View get() = rootLayout - override val revealWindow: Window get() = window - private lateinit var viewModel: EditManualConsoleViewModel + private lateinit var binding: ActivityEditManualBinding + + override val revealIntent: Intent get() = intent + override val revealRootLayout: View get() = binding.rootLayout + override val revealWindow: Window get() = window private val disposable = CompositeDisposable() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setContentView(R.layout.activity_edit_manual) + binding = ActivityEditManualBinding.inflate(layoutInflater) + setContentView(binding.root) handleReveal() viewModel = ViewModelProvider(this, viewModelFactory { @@ -52,17 +54,17 @@ class EditManualConsoleActivity: AppCompatActivity(), RevealActivity .get(EditManualConsoleViewModel::class.java) viewModel.existingHost?.observe(this, Observer { - hostEditText.setText(it.host) + binding.hostEditText.setText(it.host) }) viewModel.selectedRegisteredHost.observe(this, Observer { - registeredHostTextView.setText(titleForRegisteredHost(it)) + binding.registeredHostTextView.setText(titleForRegisteredHost(it)) }) viewModel.registeredHosts.observe(this, Observer { hosts -> - registeredHostTextView.setAdapter(ArrayAdapter(this, R.layout.dropdown_menu_popup_item, + binding.registeredHostTextView.setAdapter(ArrayAdapter(this, R.layout.dropdown_menu_popup_item, hosts.map { titleForRegisteredHost(it) })) - registeredHostTextView.onItemClickListener = object: AdapterView.OnItemClickListener { + binding.registeredHostTextView.onItemClickListener = object: AdapterView.OnItemClickListener { override fun onItemClick(parent: AdapterView<*>?, view: View?, position: Int, id: Long) { if(position >= hosts.size) @@ -73,8 +75,7 @@ class EditManualConsoleActivity: AppCompatActivity(), RevealActivity } }) - - saveButton.setOnClickListener { saveHost() } + binding.saveButton.setOnClickListener { saveHost() } } private fun titleForRegisteredHost(registeredHost: RegisteredHost?) = @@ -85,14 +86,14 @@ class EditManualConsoleActivity: AppCompatActivity(), RevealActivity private fun saveHost() { - val host = hostEditText.text.toString().trim() + val host = binding.hostEditText.text.toString().trim() if(host.isEmpty()) { - hostEditText.error = getString(R.string.entered_host_invalid) + binding.hostEditText.error = getString(R.string.entered_host_invalid) return } - saveButton.isEnabled = false + binding.saveButton.isEnabled = false viewModel.saveHost(host) .observeOn(AndroidSchedulers.mainThread()) .subscribe { diff --git a/android/app/src/main/java/com/metallic/chiaki/regist/RegistActivity.kt b/android/app/src/main/java/com/metallic/chiaki/regist/RegistActivity.kt index fb3ddaa..a8d06f6 100644 --- a/android/app/src/main/java/com/metallic/chiaki/regist/RegistActivity.kt +++ b/android/app/src/main/java/com/metallic/chiaki/regist/RegistActivity.kt @@ -12,9 +12,9 @@ import androidx.lifecycle.Observer import androidx.lifecycle.ViewModelProvider import com.metallic.chiaki.R import com.metallic.chiaki.common.ext.RevealActivity +import com.metallic.chiaki.databinding.ActivityRegistBinding import com.metallic.chiaki.lib.RegistInfo import com.metallic.chiaki.lib.Target -import kotlinx.android.synthetic.main.activity_regist.* import java.lang.IllegalArgumentException class RegistActivity: AppCompatActivity(), RevealActivity @@ -30,33 +30,35 @@ class RegistActivity: AppCompatActivity(), RevealActivity private const val REQUEST_REGIST = 1 } + private lateinit var viewModel: RegistViewModel + private lateinit var binding: ActivityRegistBinding + override val revealWindow: Window get() = window override val revealIntent: Intent get() = intent - override val revealRootLayout: View get() = rootLayout - - private lateinit var viewModel: RegistViewModel + override val revealRootLayout: View get() = binding.rootLayout override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + binding = ActivityRegistBinding.inflate(layoutInflater) setContentView(R.layout.activity_regist) handleReveal() viewModel = ViewModelProvider(this).get(RegistViewModel::class.java) - hostEditText.setText(intent.getStringExtra(EXTRA_HOST) ?: "255.255.255.255") - broadcastCheckBox.isChecked = intent.getBooleanExtra(EXTRA_BROADCAST, true) + binding.hostEditText.setText(intent.getStringExtra(EXTRA_HOST) ?: "255.255.255.255") + binding.broadcastCheckBox.isChecked = intent.getBooleanExtra(EXTRA_BROADCAST, true) - registButton.setOnClickListener { doRegist() } + binding.registButton.setOnClickListener { doRegist() } - ps4VersionRadioGroup.check(when(viewModel.ps4Version.value ?: RegistViewModel.ConsoleVersion.PS5) { + binding.ps4VersionRadioGroup.check(when(viewModel.ps4Version.value ?: RegistViewModel.ConsoleVersion.PS5) { RegistViewModel.ConsoleVersion.PS5 -> R.id.ps5RadioButton RegistViewModel.ConsoleVersion.PS4_GE_8 -> R.id.ps4VersionGE8RadioButton RegistViewModel.ConsoleVersion.PS4_GE_7 -> R.id.ps4VersionGE7RadioButton RegistViewModel.ConsoleVersion.PS4_LT_7 -> R.id.ps4VersionLT7RadioButton }) - ps4VersionRadioGroup.setOnCheckedChangeListener { _, checkedId -> + binding.ps4VersionRadioGroup.setOnCheckedChangeListener { _, checkedId -> viewModel.ps4Version.value = when(checkedId) { R.id.ps5RadioButton -> RegistViewModel.ConsoleVersion.PS5 @@ -68,14 +70,14 @@ class RegistActivity: AppCompatActivity(), RevealActivity } viewModel.ps4Version.observe(this, Observer { - psnAccountIdHelpGroup.visibility = if(it == RegistViewModel.ConsoleVersion.PS4_LT_7) View.GONE else View.VISIBLE - psnIdTextInputLayout.hint = getString(when(it!!) + binding.psnAccountIdHelpGroup.visibility = if(it == RegistViewModel.ConsoleVersion.PS4_LT_7) View.GONE else View.VISIBLE + binding.psnIdTextInputLayout.hint = getString(when(it!!) { RegistViewModel.ConsoleVersion.PS4_LT_7 -> R.string.hint_regist_psn_online_id else -> R.string.hint_regist_psn_account_id }) - pinHelpBeforeTextView.setText(if(it.isPS5) R.string.regist_pin_instructions_ps5_before else R.string.regist_pin_instructions_ps4_before) - pinHelpNavigationTextView.setText(if(it.isPS5) R.string.regist_pin_instructions_ps5_navigation else R.string.regist_pin_instructions_ps4_navigation) + binding.pinHelpBeforeTextView.setText(if(it.isPS5) R.string.regist_pin_instructions_ps5_before else R.string.regist_pin_instructions_ps4_before) + binding.pinHelpNavigationTextView.setText(if(it.isPS5) R.string.regist_pin_instructions_ps5_navigation else R.string.regist_pin_instructions_ps4_navigation) }) } @@ -83,11 +85,11 @@ class RegistActivity: AppCompatActivity(), RevealActivity { val ps4Version = viewModel.ps4Version.value ?: RegistViewModel.ConsoleVersion.PS5 - val host = hostEditText.text.toString().trim() + val host = binding.hostEditText.text.toString().trim() val hostValid = host.isNotEmpty() - val broadcast = broadcastCheckBox.isChecked + val broadcast = binding.broadcastCheckBox.isChecked - val psnId = psnIdEditText.text.toString().trim() + val psnId = binding.psnIdEditText.text.toString().trim() val psnOnlineId: String? = if(ps4Version == RegistViewModel.ConsoleVersion.PS4_LT_7) psnId else null val psnAccountId: ByteArray? = if(ps4Version != RegistViewModel.ConsoleVersion.PS4_LT_7) @@ -101,11 +103,11 @@ class RegistActivity: AppCompatActivity(), RevealActivity } - val pin = pinEditText.text.toString() + val pin = binding.pinEditText.text.toString() val pinValid = pin.length == PIN_LENGTH - hostEditText.error = if(!hostValid) getString(R.string.entered_host_invalid) else null - psnIdEditText.error = + binding.hostEditText.error = if(!hostValid) getString(R.string.entered_host_invalid) else null + binding.psnIdEditText.error = if(!psnIdValid) getString(when(ps4Version) { @@ -114,7 +116,7 @@ class RegistActivity: AppCompatActivity(), RevealActivity }) else null - pinEditText.error = if(!pinValid) getString(R.string.regist_pin_invalid, PIN_LENGTH) else null + binding.pinEditText.error = if(!pinValid) getString(R.string.regist_pin_invalid, PIN_LENGTH) else null if(!hostValid || !psnIdValid || !pinValid) return diff --git a/android/app/src/main/java/com/metallic/chiaki/regist/RegistExecuteActivity.kt b/android/app/src/main/java/com/metallic/chiaki/regist/RegistExecuteActivity.kt index 870b67e..16f545a 100644 --- a/android/app/src/main/java/com/metallic/chiaki/regist/RegistExecuteActivity.kt +++ b/android/app/src/main/java/com/metallic/chiaki/regist/RegistExecuteActivity.kt @@ -16,8 +16,8 @@ import com.metallic.chiaki.R import com.metallic.chiaki.common.MacAddress import com.metallic.chiaki.common.ext.viewModelFactory import com.metallic.chiaki.common.getDatabase +import com.metallic.chiaki.databinding.ActivityRegistExecuteBinding import com.metallic.chiaki.lib.RegistInfo -import kotlinx.android.synthetic.main.activity_regist_execute.* import kotlin.math.max class RegistExecuteActivity: AppCompatActivity() @@ -31,55 +31,57 @@ class RegistExecuteActivity: AppCompatActivity() } private lateinit var viewModel: RegistExecuteViewModel + private lateinit var binding: ActivityRegistExecuteBinding override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setContentView(R.layout.activity_regist_execute) + binding = ActivityRegistExecuteBinding.inflate(layoutInflater) + setContentView(binding.root) viewModel = ViewModelProvider(this, viewModelFactory { RegistExecuteViewModel(getDatabase(this)) }) .get(RegistExecuteViewModel::class.java) - logTextView.setHorizontallyScrolling(true) - logTextView.movementMethod = ScrollingMovementMethod() + binding.logTextView.setHorizontallyScrolling(true) + binding.logTextView.movementMethod = ScrollingMovementMethod() viewModel.logText.observe(this, Observer { - val textLayout = logTextView.layout ?: return@Observer + val textLayout = binding.logTextView.layout ?: return@Observer val lineCount = textLayout.lineCount if(lineCount < 1) return@Observer - logTextView.text = it - val scrollY = textLayout.getLineBottom(lineCount - 1) - logTextView.height + logTextView.paddingTop + logTextView.paddingBottom - logTextView.scrollTo(0, max(scrollY, 0)) + binding.logTextView.text = it + val scrollY = textLayout.getLineBottom(lineCount - 1) - binding.logTextView.height + binding.logTextView.paddingTop + binding.logTextView.paddingBottom + binding.logTextView.scrollTo(0, max(scrollY, 0)) }) viewModel.state.observe(this, Observer { - progressBar.visibility = if(it == RegistExecuteViewModel.State.RUNNING) View.VISIBLE else View.GONE + binding.progressBar.visibility = if(it == RegistExecuteViewModel.State.RUNNING) View.VISIBLE else View.GONE when(it) { RegistExecuteViewModel.State.FAILED -> { - infoTextView.visibility = View.VISIBLE - infoTextView.setText(R.string.regist_info_failed) + binding.infoTextView.visibility = View.VISIBLE + binding.infoTextView.setText(R.string.regist_info_failed) setResult(RESULT_FAILED) } RegistExecuteViewModel.State.SUCCESSFUL, RegistExecuteViewModel.State.SUCCESSFUL_DUPLICATE -> { - infoTextView.visibility = View.VISIBLE - infoTextView.setText(R.string.regist_info_success) + binding.infoTextView.visibility = View.VISIBLE + binding.infoTextView.setText(R.string.regist_info_success) setResult(RESULT_OK) if(it == RegistExecuteViewModel.State.SUCCESSFUL_DUPLICATE) showDuplicateDialog() } RegistExecuteViewModel.State.STOPPED -> { - infoTextView.visibility = View.GONE + binding.infoTextView.visibility = View.GONE setResult(Activity.RESULT_CANCELED) } - else -> infoTextView.visibility = View.GONE + else -> binding.infoTextView.visibility = View.GONE } }) - shareLogButton.setOnClickListener { + binding.shareLogButton.setOnClickListener { val log = viewModel.logText.value ?: "" Intent(Intent.ACTION_SEND).also { it.type = "text/plain" diff --git a/android/app/src/main/java/com/metallic/chiaki/session/StreamInput.kt b/android/app/src/main/java/com/metallic/chiaki/session/StreamInput.kt index 2518ce1..6fbfc31 100644 --- a/android/app/src/main/java/com/metallic/chiaki/session/StreamInput.kt +++ b/android/app/src/main/java/com/metallic/chiaki/session/StreamInput.kt @@ -19,6 +19,7 @@ class StreamInput(val context: Context, val preferences: Preferences) val controllerState = sensorControllerState or keyControllerState or motionControllerState val windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager + @Suppress("DEPRECATION") when(windowManager.defaultDisplay.rotation) { Surface.ROTATION_90 -> { @@ -175,7 +176,7 @@ class StreamInput(val context: Context, val preferences: Preferences) { if(event.source and InputDevice.SOURCE_CLASS_JOYSTICK != InputDevice.SOURCE_CLASS_JOYSTICK) return false - fun Float.signedAxis() = (this * Short.MAX_VALUE).toShort() + fun Float.signedAxis() = (this * Short.MAX_VALUE).toInt().toShort() fun Float.unsignedAxis() = (this * UByte.MAX_VALUE.toFloat()).toUInt().toUByte() motionControllerState.leftX = event.getAxisValue(MotionEvent.AXIS_X).signedAxis() motionControllerState.leftY = event.getAxisValue(MotionEvent.AXIS_Y).signedAxis() diff --git a/android/app/src/main/java/com/metallic/chiaki/settings/SettingsActivity.kt b/android/app/src/main/java/com/metallic/chiaki/settings/SettingsActivity.kt index 3a87e1b..1cf224f 100644 --- a/android/app/src/main/java/com/metallic/chiaki/settings/SettingsActivity.kt +++ b/android/app/src/main/java/com/metallic/chiaki/settings/SettingsActivity.kt @@ -9,7 +9,7 @@ import androidx.fragment.app.Fragment import androidx.preference.Preference import androidx.preference.PreferenceFragmentCompat import com.metallic.chiaki.R -import kotlinx.android.synthetic.main.activity_settings.* +import com.metallic.chiaki.databinding.ActivitySettingsBinding interface TitleFragment { @@ -18,20 +18,23 @@ interface TitleFragment class SettingsActivity: AppCompatActivity(), PreferenceFragmentCompat.OnPreferenceStartFragmentCallback { + private lateinit var binding: ActivitySettingsBinding + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setContentView(R.layout.activity_settings) + binding = ActivitySettingsBinding.inflate(layoutInflater) + setContentView(binding.root) title = "" - setSupportActionBar(toolbar) + setSupportActionBar(binding.toolbar) val rootFragment = SettingsFragment() replaceFragment(rootFragment, false) supportFragmentManager.addOnBackStackChangedListener { val titleFragment = supportFragmentManager.findFragmentById(R.id.settingsFragment) as? TitleFragment ?: return@addOnBackStackChangedListener - titleTextView.text = titleFragment.getTitle(resources) + binding.titleTextView.text = titleFragment.getTitle(resources) } - titleTextView.text = rootFragment.getTitle(resources) + binding.titleTextView.text = rootFragment.getTitle(resources) } override fun onPreferenceStartFragment(caller: PreferenceFragmentCompat, pref: Preference) = when(pref.fragment) diff --git a/android/app/src/main/java/com/metallic/chiaki/settings/SettingsFragment.kt b/android/app/src/main/java/com/metallic/chiaki/settings/SettingsFragment.kt index 7a4506e..957d1d5 100644 --- a/android/app/src/main/java/com/metallic/chiaki/settings/SettingsFragment.kt +++ b/android/app/src/main/java/com/metallic/chiaki/settings/SettingsFragment.kt @@ -16,7 +16,6 @@ import com.metallic.chiaki.common.exportAndShareAllSettings import com.metallic.chiaki.common.ext.viewModelFactory import com.metallic.chiaki.common.getDatabase import com.metallic.chiaki.common.importSettingsFromUri -import com.metallic.chiaki.lib.Codec import io.reactivex.disposables.CompositeDisposable import io.reactivex.rxkotlin.addTo diff --git a/android/app/src/main/java/com/metallic/chiaki/settings/SettingsLogsAdapter.kt b/android/app/src/main/java/com/metallic/chiaki/settings/SettingsLogsAdapter.kt index 46c8904..6acf547 100644 --- a/android/app/src/main/java/com/metallic/chiaki/settings/SettingsLogsAdapter.kt +++ b/android/app/src/main/java/com/metallic/chiaki/settings/SettingsLogsAdapter.kt @@ -2,13 +2,13 @@ package com.metallic.chiaki.settings -import android.view.View +import android.view.LayoutInflater import android.view.ViewGroup import androidx.recyclerview.widget.RecyclerView import com.metallic.chiaki.R import com.metallic.chiaki.common.LogFile import com.metallic.chiaki.common.ext.inflate -import kotlinx.android.synthetic.main.item_log_file.view.* +import com.metallic.chiaki.databinding.ItemLogFileBinding import java.text.DateFormat import java.text.SimpleDateFormat import java.util.* @@ -20,7 +20,7 @@ class SettingsLogsAdapter: RecyclerView.Adapter( private val dateFormat: DateFormat = DateFormat.getDateInstance(DateFormat.SHORT) private val timeFormat = SimpleDateFormat("HH:mm:ss:SSS", Locale.getDefault()) - class ViewHolder(itemView: View): RecyclerView.ViewHolder(itemView) + class ViewHolder(val binding: ItemLogFileBinding): RecyclerView.ViewHolder(binding.root) var logFiles: List = listOf() set(value) @@ -29,16 +29,16 @@ class SettingsLogsAdapter: RecyclerView.Adapter( notifyDataSetChanged() } - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder(parent.inflate(R.layout.item_log_file)) + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = + ViewHolder(ItemLogFileBinding.inflate(LayoutInflater.from(parent.context), parent, false)) override fun getItemCount() = logFiles.size override fun onBindViewHolder(holder: ViewHolder, position: Int) { - val view = holder.itemView val logFile = logFiles[position] - view.nameTextView.text = "${dateFormat.format(logFile.date)} ${timeFormat.format(logFile.date)}" - view.summaryTextView.text = logFile.filename - view.shareButton.setOnClickListener { shareCallback?.let { it(logFile) } } + holder.binding.nameTextView.text = "${dateFormat.format(logFile.date)} ${timeFormat.format(logFile.date)}" + holder.binding.summaryTextView.text = logFile.filename + holder.binding.shareButton.setOnClickListener { shareCallback?.let { it(logFile) } } } } \ No newline at end of file diff --git a/android/app/src/main/java/com/metallic/chiaki/settings/SettingsLogsFragment.kt b/android/app/src/main/java/com/metallic/chiaki/settings/SettingsLogsFragment.kt index 07e1e3f..0397172 100644 --- a/android/app/src/main/java/com/metallic/chiaki/settings/SettingsLogsFragment.kt +++ b/android/app/src/main/java/com/metallic/chiaki/settings/SettingsLogsFragment.kt @@ -21,29 +21,35 @@ import com.metallic.chiaki.common.LogFile import com.metallic.chiaki.common.LogManager import com.metallic.chiaki.common.ext.viewModelFactory import com.metallic.chiaki.common.fileProviderAuthority -import kotlinx.android.synthetic.main.fragment_settings_logs.* +import com.metallic.chiaki.databinding.FragmentSettingsLogsBinding class SettingsLogsFragment: AppCompatDialogFragment(), TitleFragment { private lateinit var viewModel: SettingsLogsViewModel + private var _binding: FragmentSettingsLogsBinding? = null + private val binding get() = _binding!! + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View = - inflater.inflate(R.layout.fragment_settings_logs, container, false) + FragmentSettingsLogsBinding.inflate(inflater, container, false).let { + _binding = it + it.root + } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - val context = context!! + val context = requireContext() viewModel = ViewModelProvider(this, viewModelFactory { SettingsLogsViewModel(LogManager(context)) }) .get(SettingsLogsViewModel::class.java) val adapter = SettingsLogsAdapter() - logsRecyclerView.layoutManager = LinearLayoutManager(context) - logsRecyclerView.adapter = adapter + binding.logsRecyclerView.layoutManager = LinearLayoutManager(context) + binding.logsRecyclerView.adapter = adapter adapter.shareCallback = this::shareLogFile viewModel.sessionLogs.observe(viewLifecycleOwner, Observer { adapter.logFiles = it - emptyInfoGroup.visibility = if(it.isEmpty()) View.VISIBLE else View.GONE + binding.emptyInfoGroup.visibility = if(it.isEmpty()) View.VISIBLE else View.GONE }) val itemTouchSwipeCallback = object : ItemTouchSwipeCallback(context) @@ -55,7 +61,7 @@ class SettingsLogsFragment: AppCompatDialogFragment(), TitleFragment viewModel.deleteLog(file) } } - ItemTouchHelper(itemTouchSwipeCallback).attachToRecyclerView(logsRecyclerView) + ItemTouchHelper(itemTouchSwipeCallback).attachToRecyclerView(binding.logsRecyclerView) } override fun getTitle(resources: Resources): String = resources.getString(R.string.preferences_logs_title) diff --git a/android/app/src/main/java/com/metallic/chiaki/settings/SettingsRegisteredHostsAdapter.kt b/android/app/src/main/java/com/metallic/chiaki/settings/SettingsRegisteredHostsAdapter.kt index 57a06ce..509d594 100644 --- a/android/app/src/main/java/com/metallic/chiaki/settings/SettingsRegisteredHostsAdapter.kt +++ b/android/app/src/main/java/com/metallic/chiaki/settings/SettingsRegisteredHostsAdapter.kt @@ -2,17 +2,15 @@ package com.metallic.chiaki.settings -import android.view.View +import android.view.LayoutInflater import android.view.ViewGroup import androidx.recyclerview.widget.RecyclerView -import com.metallic.chiaki.R import com.metallic.chiaki.common.RegisteredHost -import com.metallic.chiaki.common.ext.inflate -import kotlinx.android.synthetic.main.item_registered_host.view.* +import com.metallic.chiaki.databinding.ItemRegisteredHostBinding class SettingsRegisteredHostsAdapter: RecyclerView.Adapter() { - class ViewHolder(itemView: View): RecyclerView.ViewHolder(itemView) + class ViewHolder(val binding: ItemRegisteredHostBinding): RecyclerView.ViewHolder(binding.root) var hosts: List = listOf() set(value) @@ -21,15 +19,15 @@ class SettingsRegisteredHostsAdapter: RecyclerView.Adapter + binding.onScreenControlsSwitch.setOnCheckedChangeListener { _, isChecked -> viewModel.setOnScreenControlsEnabled(isChecked) showOverlay() } viewModel.touchpadOnlyEnabled.observe(this, Observer { - if(touchpadOnlySwitch.isChecked != it) - touchpadOnlySwitch.isChecked = it - if(touchpadOnlySwitch.isChecked) - onScreenControlsSwitch.isChecked = false + if(binding.touchpadOnlySwitch.isChecked != it) + binding.touchpadOnlySwitch.isChecked = it + if(binding.touchpadOnlySwitch.isChecked) + binding.onScreenControlsSwitch.isChecked = false }) - touchpadOnlySwitch.setOnCheckedChangeListener { _, isChecked -> + binding.touchpadOnlySwitch.setOnCheckedChangeListener { _, isChecked -> viewModel.setTouchpadOnlyEnabled(isChecked) showOverlay() } - displayModeToggle.addOnButtonCheckedListener { _, _, _ -> + binding.displayModeToggle.addOnButtonCheckedListener { _, _, _ -> adjustStreamViewAspect() showOverlay() } //viewModel.session.attachToTextureView(textureView) - viewModel.session.attachToSurfaceView(surfaceView) + viewModel.session.attachToSurfaceView(binding.surfaceView) viewModel.session.state.observe(this, Observer { this.stateChanged(it) }) adjustStreamViewAspect() @@ -159,14 +159,14 @@ class StreamActivity : AppCompatActivity(), View.OnSystemUiVisibilityChangeListe private fun showOverlay() { - overlay.isVisible = true - overlay.animate() + binding.overlay.isVisible = true + binding.overlay.animate() .alpha(1.0f) .setListener(object: AnimatorListenerAdapter() { override fun onAnimationEnd(animation: Animator?) { - overlay.alpha = 1.0f + binding.overlay.alpha = 1.0f } }) uiVisibilityHandler.removeCallbacks(hideSystemUIRunnable) @@ -175,13 +175,13 @@ class StreamActivity : AppCompatActivity(), View.OnSystemUiVisibilityChangeListe private fun hideOverlay() { - overlay.animate() + binding.overlay.animate() .alpha(0.0f) .setListener(object: AnimatorListenerAdapter() { override fun onAnimationEnd(animation: Animator?) { - overlay.isGone = true + binding.overlay.isGone = true } }) } @@ -214,7 +214,7 @@ class StreamActivity : AppCompatActivity(), View.OnSystemUiVisibilityChangeListe private fun stateChanged(state: StreamState) { - progressBar.visibility = if(state == StreamStateConnecting) View.VISIBLE else View.GONE + binding.progressBar.visibility = if(state == StreamStateConnecting) View.VISIBLE else View.GONE when(state) { @@ -302,7 +302,7 @@ class StreamActivity : AppCompatActivity(), View.OnSystemUiVisibilityChangeListe private fun adjustTextureViewAspect(textureView: TextureView) { val trans = TextureViewTransform(viewModel.session.connectInfo.videoProfile, textureView) - val resolution = trans.resolutionFor(TransformMode.fromButton(displayModeToggle.checkedButtonId)) + val resolution = trans.resolutionFor(TransformMode.fromButton(binding.displayModeToggle.checkedButtonId)) Matrix().also { textureView.getTransform(it) it.setScale(resolution.width / trans.viewWidth, resolution.height / trans.viewHeight) @@ -314,8 +314,8 @@ class StreamActivity : AppCompatActivity(), View.OnSystemUiVisibilityChangeListe private fun adjustSurfaceViewAspect() { val videoProfile = viewModel.session.connectInfo.videoProfile - aspectRatioLayout.aspectRatio = videoProfile.width.toFloat() / videoProfile.height.toFloat() - aspectRatioLayout.mode = TransformMode.fromButton(displayModeToggle.checkedButtonId) + binding.aspectRatioLayout.aspectRatio = videoProfile.width.toFloat() / videoProfile.height.toFloat() + binding.aspectRatioLayout.mode = TransformMode.fromButton(binding.displayModeToggle.checkedButtonId) } private fun adjustStreamViewAspect() = adjustSurfaceViewAspect() diff --git a/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchControlsFragment.kt b/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchControlsFragment.kt index 644f5f3..d3ab155 100644 --- a/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchControlsFragment.kt +++ b/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchControlsFragment.kt @@ -3,16 +3,14 @@ package com.metallic.chiaki.touchcontrols import android.os.Bundle -import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.fragment.app.Fragment import androidx.lifecycle.LiveData import androidx.lifecycle.Observer -import com.metallic.chiaki.R +import com.metallic.chiaki.databinding.FragmentControlsBinding import com.metallic.chiaki.lib.ControllerState -import kotlinx.android.synthetic.main.fragment_controls.* class TouchControlsFragment : Fragment() { @@ -28,44 +26,50 @@ class TouchControlsFragment : Fragment() var controllerStateCallback: ((ControllerState) -> Unit)? = null var onScreenControlsEnabled: LiveData? = null - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View - = inflater.inflate(R.layout.fragment_controls, container, false) + private var _binding: FragmentControlsBinding? = null + private val binding get() = _binding!! + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View = + FragmentControlsBinding.inflate(inflater, container, false).let { + _binding = it + it.root + } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - dpadView.stateChangeCallback = this::dpadStateChanged - crossButtonView.buttonPressedCallback = buttonStateChanged(ControllerState.BUTTON_CROSS) - moonButtonView.buttonPressedCallback = buttonStateChanged(ControllerState.BUTTON_MOON) - pyramidButtonView.buttonPressedCallback = buttonStateChanged(ControllerState.BUTTON_PYRAMID) - boxButtonView.buttonPressedCallback = buttonStateChanged(ControllerState.BUTTON_BOX) - l1ButtonView.buttonPressedCallback = buttonStateChanged(ControllerState.BUTTON_L1) - r1ButtonView.buttonPressedCallback = buttonStateChanged(ControllerState.BUTTON_R1) - l3ButtonView.buttonPressedCallback = buttonStateChanged(ControllerState.BUTTON_L3) - r3ButtonView.buttonPressedCallback = buttonStateChanged(ControllerState.BUTTON_R3) - optionsButtonView.buttonPressedCallback = buttonStateChanged(ControllerState.BUTTON_OPTIONS) - shareButtonView.buttonPressedCallback = buttonStateChanged(ControllerState.BUTTON_SHARE) - psButtonView.buttonPressedCallback = buttonStateChanged(ControllerState.BUTTON_PS) - touchpadButtonView.buttonPressedCallback = buttonStateChanged(ControllerState.BUTTON_TOUCHPAD) + binding.dpadView.stateChangeCallback = this::dpadStateChanged + binding.crossButtonView.buttonPressedCallback = buttonStateChanged(ControllerState.BUTTON_CROSS) + binding.moonButtonView.buttonPressedCallback = buttonStateChanged(ControllerState.BUTTON_MOON) + binding.pyramidButtonView.buttonPressedCallback = buttonStateChanged(ControllerState.BUTTON_PYRAMID) + binding.boxButtonView.buttonPressedCallback = buttonStateChanged(ControllerState.BUTTON_BOX) + binding.l1ButtonView.buttonPressedCallback = buttonStateChanged(ControllerState.BUTTON_L1) + binding.r1ButtonView.buttonPressedCallback = buttonStateChanged(ControllerState.BUTTON_R1) + binding.l3ButtonView.buttonPressedCallback = buttonStateChanged(ControllerState.BUTTON_L3) + binding.r3ButtonView.buttonPressedCallback = buttonStateChanged(ControllerState.BUTTON_R3) + binding.optionsButtonView.buttonPressedCallback = buttonStateChanged(ControllerState.BUTTON_OPTIONS) + binding.shareButtonView.buttonPressedCallback = buttonStateChanged(ControllerState.BUTTON_SHARE) + binding.psButtonView.buttonPressedCallback = buttonStateChanged(ControllerState.BUTTON_PS) + binding.touchpadButtonView.buttonPressedCallback = buttonStateChanged(ControllerState.BUTTON_TOUCHPAD) - l2ButtonView.buttonPressedCallback = { controllerState = controllerState.copy().apply { l2State = if(it) 255U else 0U } } - r2ButtonView.buttonPressedCallback = { controllerState = controllerState.copy().apply { r2State = if(it) 255U else 0U } } + binding.l2ButtonView.buttonPressedCallback = { controllerState = controllerState.copy().apply { l2State = if(it) 255U else 0U } } + binding.r2ButtonView.buttonPressedCallback = { controllerState = controllerState.copy().apply { r2State = if(it) 255U else 0U } } val quantizeStick = { f: Float -> - (Short.MAX_VALUE * f).toShort() + (Short.MAX_VALUE * f).toInt().toShort() } - leftAnalogStickView.stateChangedCallback = { controllerState = controllerState.copy().apply { + binding.leftAnalogStickView.stateChangedCallback = { controllerState = controllerState.copy().apply { leftX = quantizeStick(it.x) leftY = quantizeStick(it.y) }} - rightAnalogStickView.stateChangedCallback = { controllerState = controllerState.copy().apply { + binding.rightAnalogStickView.stateChangedCallback = { controllerState = controllerState.copy().apply { rightX = quantizeStick(it.x) rightY = quantizeStick(it.y) }} - onScreenControlsEnabled?.observe(this, Observer { + onScreenControlsEnabled?.observe(viewLifecycleOwner, Observer { view.visibility = if(it) View.VISIBLE else View.GONE }) } diff --git a/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchpadOnlyFragment.kt b/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchpadOnlyFragment.kt index 610c099..ef96b84 100644 --- a/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchpadOnlyFragment.kt +++ b/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchpadOnlyFragment.kt @@ -10,8 +10,9 @@ import androidx.fragment.app.Fragment import androidx.lifecycle.LiveData import androidx.lifecycle.Observer import com.metallic.chiaki.R +import com.metallic.chiaki.databinding.FragmentControlsBinding +import com.metallic.chiaki.databinding.FragmentTouchpadOnlyBinding import com.metallic.chiaki.lib.ControllerState -import kotlinx.android.synthetic.main.fragment_controls.* class TouchpadOnlyFragment : Fragment() { @@ -27,16 +28,22 @@ class TouchpadOnlyFragment : Fragment() var controllerStateCallback: ((ControllerState) -> Unit)? = null var touchpadOnlyEnabled: LiveData? = null - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View - = inflater.inflate(R.layout.fragment_touchpad_only, container, false) + private var _binding: FragmentTouchpadOnlyBinding? = null + private val binding get() = _binding!! + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View = + FragmentTouchpadOnlyBinding.inflate(inflater, container, false).let { + _binding = it + it.root + } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - touchpadButtonView.buttonPressedCallback = buttonStateChanged(ControllerState.BUTTON_TOUCHPAD) + binding.touchpadButtonView.buttonPressedCallback = buttonStateChanged(ControllerState.BUTTON_TOUCHPAD) - touchpadOnlyEnabled?.observe(this, Observer { + touchpadOnlyEnabled?.observe(viewLifecycleOwner, Observer { view.visibility = if(it) View.VISIBLE else View.GONE }) } From 5914ceec77aa545e4995377f30d4c80d0cef3260 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Fri, 15 Jan 2021 11:34:10 +0100 Subject: [PATCH 184/237] Add TouchpadView to Android --- .../java/com/metallic/chiaki/lib/Chiaki.kt | 38 +++++++- .../chiaki/touchcontrols/TouchTracker.kt | 2 +- .../chiaki/touchcontrols/TouchpadView.kt | 86 +++++++++++++++++++ .../main/res/drawable/control_touchpad.xml | 12 +++ .../src/main/res/layout/fragment_controls.xml | 10 +++ android/app/src/main/res/values/attrs.xml | 4 + assets/controls/touchpad_surface.svg | 65 ++++++++++++++ 7 files changed, 212 insertions(+), 5 deletions(-) create mode 100644 android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchpadView.kt create mode 100644 android/app/src/main/res/drawable/control_touchpad.xml create mode 100644 assets/controls/touchpad_surface.svg diff --git a/android/app/src/main/java/com/metallic/chiaki/lib/Chiaki.kt b/android/app/src/main/java/com/metallic/chiaki/lib/Chiaki.kt index 9533c7c..9c4ea34 100644 --- a/android/app/src/main/java/com/metallic/chiaki/lib/Chiaki.kt +++ b/android/app/src/main/java/com/metallic/chiaki/lib/Chiaki.kt @@ -152,9 +152,9 @@ private fun maxAbs(a: Short, b: Short) = if(abs(a.toInt()) > abs(b.toInt())) a e private val CONTROLLER_TOUCHES_MAX = 2 // must be the same as CHIAKI_CONTROLLER_TOUCHES_MAX data class ControllerTouch( - val x: UShort = 0U, - val y: UShort = 0U, - val id: Byte = -1 // -1 = up + var x: UShort = 0U, + var y: UShort = 0U, + var id: Byte = -1 // -1 = up ) data class ControllerState constructor( @@ -165,7 +165,7 @@ data class ControllerState constructor( var leftY: Short = 0, var rightX: Short = 0, var rightY: Short = 0, - private var touchIdNext: UByte = 0U, + private var touchIdNext: UByte = 100U, var touches: Array = arrayOf(ControllerTouch(), ControllerTouch()), var gyroX: Float = 0.0f, var gyroY: Float = 0.0f, @@ -196,6 +196,8 @@ data class ControllerState constructor( val BUTTON_SHARE = (1 shl 13).toUInt() val BUTTON_TOUCHPAD = (1 shl 14).toUInt() val BUTTON_PS = (1 shl 15).toUInt() + val TOUCHPAD_WIDTH: UShort = 1920U + val TOUCHPAD_HEIGHT: UShort = 942U } infix fun or(o: ControllerState) = ControllerState( @@ -272,6 +274,34 @@ data class ControllerState constructor( result = 31 * result + orientW.hashCode() return result } + + fun startTouch(x: UShort, y: UShort): UByte? = + touches + .find { it.id < 0 } + ?.also { + it.id = touchIdNext.toByte() + Log.d("TouchId", "touch id next: $touchIdNext") + touchIdNext = ((touchIdNext + 1U) and 0x7fU).toUByte() + }?.id?.toUByte() + + fun stopTouch(id: UByte) + { + touches.find { + it.id >= 0 && it.id == id.toByte() + }?.let { + it.id = -1 + } + } + + fun setTouchPos(id: UByte, x: UShort, y: UShort): Boolean + = touches.find { + it.id >= 0 && it.id == id.toByte() + }?.let { + val r = it.x != x || it.y != y + it.x = x + it.y = y + r + } ?: false } class QuitReason(val value: Int) diff --git a/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchTracker.kt b/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchTracker.kt index 01d6869..a1ead71 100644 --- a/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchTracker.kt +++ b/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchTracker.kt @@ -26,7 +26,7 @@ class TouchTracker if(pointerId == null) { pointerId = event.getPointerId(event.actionIndex) - currentPosition = Vector(event.x, event.y) + currentPosition = Vector(event.getX(event.actionIndex), event.getY(event.actionIndex)) } } diff --git a/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchpadView.kt b/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchpadView.kt new file mode 100644 index 0000000..292351b --- /dev/null +++ b/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchpadView.kt @@ -0,0 +1,86 @@ +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL + +package com.metallic.chiaki.touchcontrols + +import android.content.Context +import android.graphics.Canvas +import android.graphics.drawable.Drawable +import android.util.AttributeSet +import android.view.MotionEvent +import android.view.View +import com.metallic.chiaki.R +import com.metallic.chiaki.lib.ControllerState + +class TouchpadView @JvmOverloads constructor( + context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 +) : View(context, attrs, defStyleAttr) +{ + val state: ControllerState = ControllerState() + private val pointerTouchIds = mutableMapOf() + + var stateChangeCallback: ((ControllerState) -> Unit)? = null + + private val drawable: Drawable? + + init + { + context.theme.obtainStyledAttributes(attrs, R.styleable.TouchpadView, 0, 0).apply { + drawable = getDrawable(R.styleable.TouchpadView_drawable) + recycle() + } + isClickable = true + } + + override fun onDraw(canvas: Canvas) + { + super.onDraw(canvas) + if(state.touches.find { it.id >= 0 } == null) + return + drawable?.setBounds(paddingLeft, paddingTop, width - paddingRight, height - paddingBottom) + drawable?.draw(canvas) + } + + private fun touchX(event: MotionEvent, index: Int): UShort = + maxOf(0U.toUShort(), minOf((ControllerState.TOUCHPAD_WIDTH - 1u).toUShort(), + (ControllerState.TOUCHPAD_WIDTH.toFloat() * event.getX(index) / width.toFloat()).toUInt().toUShort())) + + private fun touchY(event: MotionEvent, index: Int): UShort = + maxOf(0U.toUShort(), minOf((ControllerState.TOUCHPAD_HEIGHT - 1u).toUShort(), + (ControllerState.TOUCHPAD_HEIGHT.toFloat() * event.getY(index) / height.toFloat()).toUInt().toUShort())) + + override fun onTouchEvent(event: MotionEvent): Boolean + { + when(event.actionMasked) + { + MotionEvent.ACTION_DOWN, MotionEvent.ACTION_POINTER_DOWN -> { + state.startTouch(touchX(event, event.actionIndex), touchY(event, event.actionIndex))?.let { + pointerTouchIds[event.getPointerId(event.actionIndex)] = it + triggerStateChanged() + } + } + MotionEvent.ACTION_UP, MotionEvent.ACTION_POINTER_UP -> { + pointerTouchIds.remove(event.getPointerId(event.actionIndex))?.let { + state.stopTouch(it) + triggerStateChanged() + } + } + MotionEvent.ACTION_MOVE -> { + val changed = pointerTouchIds.entries.fold(false) { acc, it -> + val index = event.findPointerIndex(it.key) + if(index < 0) + acc + else + acc || state.setTouchPos(it.value, touchX(event, index), touchY(event, index)) + } + if(changed) + triggerStateChanged() + } + } + return true + } + + private fun triggerStateChanged() + { + stateChangeCallback?.let { it(state) } + } +} \ No newline at end of file diff --git a/android/app/src/main/res/drawable/control_touchpad.xml b/android/app/src/main/res/drawable/control_touchpad.xml new file mode 100644 index 0000000..509b8a3 --- /dev/null +++ b/android/app/src/main/res/drawable/control_touchpad.xml @@ -0,0 +1,12 @@ + + + diff --git a/android/app/src/main/res/layout/fragment_controls.xml b/android/app/src/main/res/layout/fragment_controls.xml index 94c2c52..1f33197 100644 --- a/android/app/src/main/res/layout/fragment_controls.xml +++ b/android/app/src/main/res/layout/fragment_controls.xml @@ -47,6 +47,16 @@ app:drawableHandle="@drawable/control_analog_stick_handle" /> + + + + + + \ No newline at end of file diff --git a/assets/controls/touchpad_surface.svg b/assets/controls/touchpad_surface.svg new file mode 100644 index 0000000..7476128 --- /dev/null +++ b/assets/controls/touchpad_surface.svg @@ -0,0 +1,65 @@ + + + + + + + + image/svg+xml + + + + + + + + + From fa44a3269c1e9ba6120adb343d9bb555c95ff2bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Fri, 15 Jan 2021 15:32:37 +0100 Subject: [PATCH 185/237] Propagate Touchpad State through TouchControlsFragment on Android --- .../metallic/chiaki/stream/StreamActivity.kt | 31 ++++++++++++----- .../touchcontrols/TouchControlsFragment.kt | 33 ++++++++++++++----- .../chiaki/touchcontrols/TouchpadView.kt | 11 +++++-- .../src/main/res/layout/fragment_controls.xml | 1 + 4 files changed, 56 insertions(+), 20 deletions(-) diff --git a/android/app/src/main/java/com/metallic/chiaki/stream/StreamActivity.kt b/android/app/src/main/java/com/metallic/chiaki/stream/StreamActivity.kt index def22f4..fd07ff4 100644 --- a/android/app/src/main/java/com/metallic/chiaki/stream/StreamActivity.kt +++ b/android/app/src/main/java/com/metallic/chiaki/stream/StreamActivity.kt @@ -24,6 +24,8 @@ import com.metallic.chiaki.lib.ConnectVideoProfile import com.metallic.chiaki.session.* import com.metallic.chiaki.touchcontrols.TouchControlsFragment import com.metallic.chiaki.touchcontrols.TouchpadOnlyFragment +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.rxkotlin.addTo import kotlin.math.min private sealed class DialogContents @@ -113,18 +115,25 @@ class StreamActivity : AppCompatActivity(), View.OnSystemUiVisibilityChangeListe } } + private val controlsDisposable = CompositeDisposable() + override fun onAttachFragment(fragment: Fragment) { super.onAttachFragment(fragment) - if(fragment is TouchControlsFragment) + when(fragment) { - fragment.controllerStateCallback = { viewModel.input.touchControllerState = it } - fragment.onScreenControlsEnabled = viewModel.onScreenControlsEnabled - } - if(fragment is TouchpadOnlyFragment) - { - fragment.controllerStateCallback = { viewModel.input.touchControllerState = it } - fragment.touchpadOnlyEnabled = viewModel.touchpadOnlyEnabled + is TouchControlsFragment -> + { + fragment.controllerState + .subscribe { viewModel.input.touchControllerState = it } + .addTo(controlsDisposable) + fragment.onScreenControlsEnabled = viewModel.onScreenControlsEnabled + } + is TouchpadOnlyFragment -> + { + fragment.controllerStateCallback = { viewModel.input.touchControllerState = it } + fragment.touchpadOnlyEnabled = viewModel.touchpadOnlyEnabled + } } } @@ -141,6 +150,12 @@ class StreamActivity : AppCompatActivity(), View.OnSystemUiVisibilityChangeListe viewModel.session.pause() } + override fun onDestroy() + { + super.onDestroy() + controlsDisposable.dispose() + } + private fun reconnect() { viewModel.session.shutdown() diff --git a/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchControlsFragment.kt b/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchControlsFragment.kt index d3ab155..ad734ce 100644 --- a/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchControlsFragment.kt +++ b/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchControlsFragment.kt @@ -11,19 +11,31 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.Observer import com.metallic.chiaki.databinding.FragmentControlsBinding import com.metallic.chiaki.lib.ControllerState +import io.reactivex.Observable +import io.reactivex.Observable.combineLatest +import io.reactivex.subjects.BehaviorSubject +import io.reactivex.subjects.Subject class TouchControlsFragment : Fragment() { - private var controllerState = ControllerState() + private var ownControllerState = ControllerState() private set(value) { val diff = field != value field = value if(diff) - controllerStateCallback?.let { it(value) } + ownControllerStateSubject.onNext(ownControllerState) } - var controllerStateCallback: ((ControllerState) -> Unit)? = null + private val ownControllerStateSubject: Subject + = BehaviorSubject.create().also { it.onNext(ownControllerState) } + + // to delay attaching to the touchpadView until it's available + private val controllerStateProxy: Subject> + = BehaviorSubject.create>().also { it.onNext(ownControllerStateSubject) } + val controllerState: Observable get() = + controllerStateProxy.flatMap { it } + var onScreenControlsEnabled: LiveData? = null private var _binding: FragmentControlsBinding? = null @@ -32,6 +44,9 @@ class TouchControlsFragment : Fragment() override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View = FragmentControlsBinding.inflate(inflater, container, false).let { _binding = it + controllerStateProxy.onNext( + combineLatest(ownControllerStateSubject, binding.touchpadView.controllerState) { a, b -> a or b } + ) it.root } @@ -52,19 +67,19 @@ class TouchControlsFragment : Fragment() binding.psButtonView.buttonPressedCallback = buttonStateChanged(ControllerState.BUTTON_PS) binding.touchpadButtonView.buttonPressedCallback = buttonStateChanged(ControllerState.BUTTON_TOUCHPAD) - binding.l2ButtonView.buttonPressedCallback = { controllerState = controllerState.copy().apply { l2State = if(it) 255U else 0U } } - binding.r2ButtonView.buttonPressedCallback = { controllerState = controllerState.copy().apply { r2State = if(it) 255U else 0U } } + binding.l2ButtonView.buttonPressedCallback = { ownControllerState = ownControllerState.copy().apply { l2State = if(it) 255U else 0U } } + binding.r2ButtonView.buttonPressedCallback = { ownControllerState = ownControllerState.copy().apply { r2State = if(it) 255U else 0U } } val quantizeStick = { f: Float -> (Short.MAX_VALUE * f).toInt().toShort() } - binding.leftAnalogStickView.stateChangedCallback = { controllerState = controllerState.copy().apply { + binding.leftAnalogStickView.stateChangedCallback = { ownControllerState = ownControllerState.copy().apply { leftX = quantizeStick(it.x) leftY = quantizeStick(it.y) }} - binding.rightAnalogStickView.stateChangedCallback = { controllerState = controllerState.copy().apply { + binding.rightAnalogStickView.stateChangedCallback = { ownControllerState = ownControllerState.copy().apply { rightX = quantizeStick(it.x) rightY = quantizeStick(it.y) }} @@ -76,7 +91,7 @@ class TouchControlsFragment : Fragment() private fun dpadStateChanged(direction: DPadView.Direction?) { - controllerState = controllerState.copy().apply { + ownControllerState = ownControllerState.copy().apply { buttons = ((buttons and ControllerState.BUTTON_DPAD_LEFT.inv() and ControllerState.BUTTON_DPAD_RIGHT.inv() @@ -98,7 +113,7 @@ class TouchControlsFragment : Fragment() } private fun buttonStateChanged(buttonMask: UInt) = { pressed: Boolean -> - controllerState = controllerState.copy().apply { + ownControllerState = ownControllerState.copy().apply { buttons = if(pressed) buttons or buttonMask diff --git a/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchpadView.kt b/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchpadView.kt index 292351b..8c8f44f 100644 --- a/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchpadView.kt +++ b/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchpadView.kt @@ -10,15 +10,20 @@ import android.view.MotionEvent import android.view.View import com.metallic.chiaki.R import com.metallic.chiaki.lib.ControllerState +import io.reactivex.Observable +import io.reactivex.subjects.BehaviorSubject +import io.reactivex.subjects.Subject class TouchpadView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 ) : View(context, attrs, defStyleAttr) { - val state: ControllerState = ControllerState() + private val state: ControllerState = ControllerState() private val pointerTouchIds = mutableMapOf() - var stateChangeCallback: ((ControllerState) -> Unit)? = null + private val stateSubject: Subject + = BehaviorSubject.create().also { it.onNext(state) } + val controllerState: Observable get() = stateSubject private val drawable: Drawable? @@ -81,6 +86,6 @@ class TouchpadView @JvmOverloads constructor( private fun triggerStateChanged() { - stateChangeCallback?.let { it(state) } + stateSubject.onNext(state) } } \ No newline at end of file diff --git a/android/app/src/main/res/layout/fragment_controls.xml b/android/app/src/main/res/layout/fragment_controls.xml index 1f33197..ee2a4a8 100644 --- a/android/app/src/main/res/layout/fragment_controls.xml +++ b/android/app/src/main/res/layout/fragment_controls.xml @@ -48,6 +48,7 @@ /> Date: Fri, 15 Jan 2021 17:00:05 +0100 Subject: [PATCH 186/237] Finish Basic Touchpad on Android --- .../java/com/metallic/chiaki/lib/Chiaki.kt | 5 +++-- .../metallic/chiaki/stream/StreamActivity.kt | 19 ++++++---------- .../touchcontrols/TouchControlsFragment.kt | 17 ++++++++------ .../touchcontrols/TouchpadOnlyFragment.kt | 22 +++++-------------- .../chiaki/touchcontrols/TouchpadView.kt | 1 + .../src/main/res/layout/activity_stream.xml | 2 +- .../res/layout/fragment_touchpad_only.xml | 22 +++++++++++++++---- 7 files changed, 46 insertions(+), 42 deletions(-) diff --git a/android/app/src/main/java/com/metallic/chiaki/lib/Chiaki.kt b/android/app/src/main/java/com/metallic/chiaki/lib/Chiaki.kt index 9c4ea34..2c474f0 100644 --- a/android/app/src/main/java/com/metallic/chiaki/lib/Chiaki.kt +++ b/android/app/src/main/java/com/metallic/chiaki/lib/Chiaki.kt @@ -165,7 +165,7 @@ data class ControllerState constructor( var leftY: Short = 0, var rightX: Short = 0, var rightY: Short = 0, - private var touchIdNext: UByte = 100U, + private var touchIdNext: UByte = 0U, var touches: Array = arrayOf(ControllerTouch(), ControllerTouch()), var gyroX: Float = 0.0f, var gyroY: Float = 0.0f, @@ -280,7 +280,8 @@ data class ControllerState constructor( .find { it.id < 0 } ?.also { it.id = touchIdNext.toByte() - Log.d("TouchId", "touch id next: $touchIdNext") + it.x = x + it.y = y touchIdNext = ((touchIdNext + 1U) and 0x7fU).toUByte() }?.id?.toUByte() diff --git a/android/app/src/main/java/com/metallic/chiaki/stream/StreamActivity.kt b/android/app/src/main/java/com/metallic/chiaki/stream/StreamActivity.kt index fd07ff4..b0248be 100644 --- a/android/app/src/main/java/com/metallic/chiaki/stream/StreamActivity.kt +++ b/android/app/src/main/java/com/metallic/chiaki/stream/StreamActivity.kt @@ -22,6 +22,7 @@ import com.metallic.chiaki.databinding.ActivityStreamBinding import com.metallic.chiaki.lib.ConnectInfo import com.metallic.chiaki.lib.ConnectVideoProfile import com.metallic.chiaki.session.* +import com.metallic.chiaki.touchcontrols.DefaultTouchControlsFragment import com.metallic.chiaki.touchcontrols.TouchControlsFragment import com.metallic.chiaki.touchcontrols.TouchpadOnlyFragment import io.reactivex.disposables.CompositeDisposable @@ -120,20 +121,14 @@ class StreamActivity : AppCompatActivity(), View.OnSystemUiVisibilityChangeListe override fun onAttachFragment(fragment: Fragment) { super.onAttachFragment(fragment) - when(fragment) + if(fragment is TouchControlsFragment) { - is TouchControlsFragment -> - { - fragment.controllerState - .subscribe { viewModel.input.touchControllerState = it } - .addTo(controlsDisposable) - fragment.onScreenControlsEnabled = viewModel.onScreenControlsEnabled - } - is TouchpadOnlyFragment -> - { - fragment.controllerStateCallback = { viewModel.input.touchControllerState = it } + fragment.controllerState + .subscribe { viewModel.input.touchControllerState = it } + .addTo(controlsDisposable) + fragment.onScreenControlsEnabled = viewModel.onScreenControlsEnabled + if(fragment is TouchpadOnlyFragment) fragment.touchpadOnlyEnabled = viewModel.touchpadOnlyEnabled - } } } diff --git a/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchControlsFragment.kt b/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchControlsFragment.kt index ad734ce..b3ab922 100644 --- a/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchControlsFragment.kt +++ b/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchControlsFragment.kt @@ -12,14 +12,14 @@ import androidx.lifecycle.Observer import com.metallic.chiaki.databinding.FragmentControlsBinding import com.metallic.chiaki.lib.ControllerState import io.reactivex.Observable -import io.reactivex.Observable.combineLatest +import io.reactivex.rxkotlin.Observables.combineLatest import io.reactivex.subjects.BehaviorSubject import io.reactivex.subjects.Subject -class TouchControlsFragment : Fragment() +abstract class TouchControlsFragment : Fragment() { - private var ownControllerState = ControllerState() - private set(value) + protected var ownControllerState = ControllerState() + set(value) { val diff = field != value field = value @@ -27,17 +27,20 @@ class TouchControlsFragment : Fragment() ownControllerStateSubject.onNext(ownControllerState) } - private val ownControllerStateSubject: Subject + protected val ownControllerStateSubject: Subject = BehaviorSubject.create().also { it.onNext(ownControllerState) } // to delay attaching to the touchpadView until it's available - private val controllerStateProxy: Subject> - = BehaviorSubject.create>().also { it.onNext(ownControllerStateSubject) } + protected val controllerStateProxy: Subject> + = BehaviorSubject.create>().also { it.onNext(ownControllerStateSubject) } val controllerState: Observable get() = controllerStateProxy.flatMap { it } var onScreenControlsEnabled: LiveData? = null +} +class DefaultTouchControlsFragment : TouchControlsFragment() +{ private var _binding: FragmentControlsBinding? = null private val binding get() = _binding!! diff --git a/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchpadOnlyFragment.kt b/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchpadOnlyFragment.kt index ef96b84..788b6d5 100644 --- a/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchpadOnlyFragment.kt +++ b/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchpadOnlyFragment.kt @@ -6,26 +6,14 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.fragment.app.Fragment import androidx.lifecycle.LiveData import androidx.lifecycle.Observer -import com.metallic.chiaki.R -import com.metallic.chiaki.databinding.FragmentControlsBinding import com.metallic.chiaki.databinding.FragmentTouchpadOnlyBinding import com.metallic.chiaki.lib.ControllerState +import io.reactivex.rxkotlin.Observables.combineLatest -class TouchpadOnlyFragment : Fragment() +class TouchpadOnlyFragment : TouchControlsFragment() { - private var controllerState = ControllerState() - private set(value) - { - val diff = field != value - field = value - if(diff) - controllerStateCallback?.let { it(value) } - } - - var controllerStateCallback: ((ControllerState) -> Unit)? = null var touchpadOnlyEnabled: LiveData? = null private var _binding: FragmentTouchpadOnlyBinding? = null @@ -34,6 +22,9 @@ class TouchpadOnlyFragment : Fragment() override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View = FragmentTouchpadOnlyBinding.inflate(inflater, container, false).let { _binding = it + controllerStateProxy.onNext( + combineLatest(ownControllerStateSubject, binding.touchpadView.controllerState) { a, b -> a or b } + ) it.root } @@ -49,13 +40,12 @@ class TouchpadOnlyFragment : Fragment() } private fun buttonStateChanged(buttonMask: UInt) = { pressed: Boolean -> - controllerState = controllerState.copy().apply { + ownControllerState = ownControllerState.copy().apply { buttons = if(pressed) buttons or buttonMask else buttons and buttonMask.inv() - } } } \ No newline at end of file diff --git a/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchpadView.kt b/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchpadView.kt index 8c8f44f..b712c6e 100644 --- a/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchpadView.kt +++ b/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchpadView.kt @@ -86,6 +86,7 @@ class TouchpadView @JvmOverloads constructor( private fun triggerStateChanged() { + invalidate() stateSubject.onNext(state) } } \ No newline at end of file diff --git a/android/app/src/main/res/layout/activity_stream.xml b/android/app/src/main/res/layout/activity_stream.xml index 99eea1b..6a7f9a3 100644 --- a/android/app/src/main/res/layout/activity_stream.xml +++ b/android/app/src/main/res/layout/activity_stream.xml @@ -28,7 +28,7 @@ diff --git a/android/app/src/main/res/layout/fragment_touchpad_only.xml b/android/app/src/main/res/layout/fragment_touchpad_only.xml index a42b78a..2da7a09 100644 --- a/android/app/src/main/res/layout/fragment_touchpad_only.xml +++ b/android/app/src/main/res/layout/fragment_touchpad_only.xml @@ -1,5 +1,6 @@ - + + \ No newline at end of file From bae081d5b319385ce6023bd4cc762f6c20f722e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Fri, 15 Jan 2021 18:00:05 +0100 Subject: [PATCH 187/237] Trigger Touchpad Button from TouchpadView on Android --- .../touchcontrols/TouchControlsFragment.kt | 1 - .../touchcontrols/TouchpadOnlyFragment.kt | 4 - .../chiaki/touchcontrols/TouchpadView.kt | 89 +++++++++++++++++-- .../res/drawable/control_button_touchpad.xml | 12 --- .../control_button_touchpad_pressed.xml | 12 --- .../res/drawable/control_touchpad_pressed.xml | 12 +++ .../src/main/res/layout/fragment_controls.xml | 17 +--- .../res/layout/fragment_touchpad_only.xml | 15 +--- android/app/src/main/res/values/attrs.xml | 10 ++- 9 files changed, 105 insertions(+), 67 deletions(-) delete mode 100644 android/app/src/main/res/drawable/control_button_touchpad.xml delete mode 100644 android/app/src/main/res/drawable/control_button_touchpad_pressed.xml create mode 100644 android/app/src/main/res/drawable/control_touchpad_pressed.xml diff --git a/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchControlsFragment.kt b/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchControlsFragment.kt index b3ab922..b1ecfbe 100644 --- a/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchControlsFragment.kt +++ b/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchControlsFragment.kt @@ -68,7 +68,6 @@ class DefaultTouchControlsFragment : TouchControlsFragment() binding.optionsButtonView.buttonPressedCallback = buttonStateChanged(ControllerState.BUTTON_OPTIONS) binding.shareButtonView.buttonPressedCallback = buttonStateChanged(ControllerState.BUTTON_SHARE) binding.psButtonView.buttonPressedCallback = buttonStateChanged(ControllerState.BUTTON_PS) - binding.touchpadButtonView.buttonPressedCallback = buttonStateChanged(ControllerState.BUTTON_TOUCHPAD) binding.l2ButtonView.buttonPressedCallback = { ownControllerState = ownControllerState.copy().apply { l2State = if(it) 255U else 0U } } binding.r2ButtonView.buttonPressedCallback = { ownControllerState = ownControllerState.copy().apply { r2State = if(it) 255U else 0U } } diff --git a/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchpadOnlyFragment.kt b/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchpadOnlyFragment.kt index 788b6d5..8910ef7 100644 --- a/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchpadOnlyFragment.kt +++ b/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchpadOnlyFragment.kt @@ -9,7 +9,6 @@ import android.view.ViewGroup import androidx.lifecycle.LiveData import androidx.lifecycle.Observer import com.metallic.chiaki.databinding.FragmentTouchpadOnlyBinding -import com.metallic.chiaki.lib.ControllerState import io.reactivex.rxkotlin.Observables.combineLatest class TouchpadOnlyFragment : TouchControlsFragment() @@ -31,9 +30,6 @@ class TouchpadOnlyFragment : TouchControlsFragment() override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - - binding.touchpadButtonView.buttonPressedCallback = buttonStateChanged(ControllerState.BUTTON_TOUCHPAD) - touchpadOnlyEnabled?.observe(viewLifecycleOwner, Observer { view.visibility = if(it) View.VISIBLE else View.GONE }) diff --git a/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchpadView.kt b/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchpadView.kt index b712c6e..9dba03d 100644 --- a/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchpadView.kt +++ b/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchpadView.kt @@ -13,24 +13,69 @@ import com.metallic.chiaki.lib.ControllerState import io.reactivex.Observable import io.reactivex.subjects.BehaviorSubject import io.reactivex.subjects.Subject +import kotlin.math.max class TouchpadView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 ) : View(context, attrs, defStyleAttr) { + companion object + { + private const val BUTTON_PRESS_MAX_MOVE_DIST_DP = 32.0f + private const val SHORT_BUTTON_PRESS_DURATION_MS = 200L + private const val BUTTON_HOLD_DELAY_MS = 500L + } + + private val drawableIdle: Drawable? + private val drawablePressed: Drawable? + private val state: ControllerState = ControllerState() - private val pointerTouchIds = mutableMapOf() + + inner class Touch( + val stateId: UByte, + private val startX: Float, + private val startY: Float) + { + var lifted = false // will be true but touch still in list when only relevant for short touch + private var maxDist: Float = 0.0f + val moveInsignificant: Boolean get() = maxDist < BUTTON_PRESS_MAX_MOVE_DIST_DP + + fun onMove(x: Float, y: Float) + { + val d = (Vector(x, y) - Vector(startX, startY)).length / resources.displayMetrics.density + maxDist = max(d, maxDist) + } + + val startButtonHoldRunnable = Runnable { + if(!moveInsignificant || buttonHeld) + return@Runnable + state.buttons = state.buttons or ControllerState.BUTTON_TOUCHPAD + buttonHeld = true + } + } + private val pointerTouches = mutableMapOf() private val stateSubject: Subject = BehaviorSubject.create().also { it.onNext(state) } val controllerState: Observable get() = stateSubject - private val drawable: Drawable? + private var shortPressingTouches = listOf() + private val shortButtonPressLiftRunnable = Runnable { + state.buttons = state.buttons and ControllerState.BUTTON_TOUCHPAD.inv() + shortPressingTouches.forEach { + state.stopTouch(it.stateId) + } + shortPressingTouches = listOf() + triggerStateChanged() + } + + private var buttonHeld = false init { context.theme.obtainStyledAttributes(attrs, R.styleable.TouchpadView, 0, 0).apply { - drawable = getDrawable(R.styleable.TouchpadView_drawable) + drawableIdle = getDrawable(R.styleable.TouchpadView_drawableIdle) + drawablePressed = getDrawable(R.styleable.TouchpadView_drawablePressed) recycle() } isClickable = true @@ -39,8 +84,9 @@ class TouchpadView @JvmOverloads constructor( override fun onDraw(canvas: Canvas) { super.onDraw(canvas) - if(state.touches.find { it.id >= 0 } == null) + if(pointerTouches.values.find { !it.lifted } == null) return + val drawable = if(state.buttons and ControllerState.BUTTON_TOUCHPAD != 0U) drawablePressed else drawableIdle drawable?.setBounds(paddingLeft, paddingTop, width - paddingRight, height - paddingBottom) drawable?.draw(canvas) } @@ -59,23 +105,40 @@ class TouchpadView @JvmOverloads constructor( { MotionEvent.ACTION_DOWN, MotionEvent.ACTION_POINTER_DOWN -> { state.startTouch(touchX(event, event.actionIndex), touchY(event, event.actionIndex))?.let { - pointerTouchIds[event.getPointerId(event.actionIndex)] = it + val touch = Touch(it, event.getX(event.actionIndex), event.getY(event.actionIndex)) + pointerTouches[event.getPointerId(event.actionIndex)] = touch + if(!buttonHeld) + postDelayed(touch.startButtonHoldRunnable, BUTTON_HOLD_DELAY_MS) triggerStateChanged() } } MotionEvent.ACTION_UP, MotionEvent.ACTION_POINTER_UP -> { - pointerTouchIds.remove(event.getPointerId(event.actionIndex))?.let { - state.stopTouch(it) + pointerTouches.remove(event.getPointerId(event.actionIndex))?.let { + removeCallbacks(it.startButtonHoldRunnable) + when + { + buttonHeld -> + { + buttonHeld = false + state.buttons = state.buttons and ControllerState.BUTTON_TOUCHPAD.inv() + state.stopTouch(it.stateId) + } + it.moveInsignificant -> triggerShortButtonPress(it) + else -> state.stopTouch(it.stateId) + } triggerStateChanged() } } MotionEvent.ACTION_MOVE -> { - val changed = pointerTouchIds.entries.fold(false) { acc, it -> + val changed = pointerTouches.entries.fold(false) { acc, it -> val index = event.findPointerIndex(it.key) if(index < 0) acc else - acc || state.setTouchPos(it.value, touchX(event, index), touchY(event, index)) + { + it.value.onMove(event.getX(event.actionIndex), event.getY(event.actionIndex)) + acc || state.setTouchPos(it.value.stateId, touchX(event, index), touchY(event, index)) + } } if(changed) triggerStateChanged() @@ -84,6 +147,14 @@ class TouchpadView @JvmOverloads constructor( return true } + private fun triggerShortButtonPress(touch: Touch) + { + shortPressingTouches = shortPressingTouches + listOf(touch) + removeCallbacks(shortButtonPressLiftRunnable) + state.buttons = state.buttons or ControllerState.BUTTON_TOUCHPAD + postDelayed(shortButtonPressLiftRunnable, SHORT_BUTTON_PRESS_DURATION_MS) + } + private fun triggerStateChanged() { invalidate() diff --git a/android/app/src/main/res/drawable/control_button_touchpad.xml b/android/app/src/main/res/drawable/control_button_touchpad.xml deleted file mode 100644 index 40c0465..0000000 --- a/android/app/src/main/res/drawable/control_button_touchpad.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - diff --git a/android/app/src/main/res/drawable/control_button_touchpad_pressed.xml b/android/app/src/main/res/drawable/control_button_touchpad_pressed.xml deleted file mode 100644 index 8255860..0000000 --- a/android/app/src/main/res/drawable/control_button_touchpad_pressed.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - diff --git a/android/app/src/main/res/drawable/control_touchpad_pressed.xml b/android/app/src/main/res/drawable/control_touchpad_pressed.xml new file mode 100644 index 0000000..b4f3f16 --- /dev/null +++ b/android/app/src/main/res/drawable/control_touchpad_pressed.xml @@ -0,0 +1,12 @@ + + + diff --git a/android/app/src/main/res/layout/fragment_controls.xml b/android/app/src/main/res/layout/fragment_controls.xml index ee2a4a8..244956a 100644 --- a/android/app/src/main/res/layout/fragment_controls.xml +++ b/android/app/src/main/res/layout/fragment_controls.xml @@ -51,8 +51,10 @@ android:id="@+id/touchpadView" android:layout_width="0dp" android:layout_height="0dp" - app:drawable="@drawable/control_touchpad" - app:layout_constraintTop_toBottomOf="@id/touchpadButtonView" + app:drawableIdle="@drawable/control_touchpad" + app:drawablePressed="@drawable/control_touchpad_pressed" + android:layout_marginTop="32dp" + app:layout_constraintTop_toTopOf="parent" app:layout_constraintWidth_max="300dp" app:layout_constraintDimensionRatio="1920:942" app:layout_constraintRight_toRightOf="parent" @@ -169,17 +171,6 @@ app:layout_constraintBottom_toBottomOf="parent"/> - - - - + + + - - + + @@ -13,6 +16,7 @@ - + + \ No newline at end of file From 28f017d6408bad5e5b8a91c8f0020a86f631ad19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Fri, 15 Jan 2021 18:23:20 +0100 Subject: [PATCH 188/237] Add Option to disable Motion on Android --- .../main/java/com/metallic/chiaki/common/Preferences.kt | 5 +++++ .../main/java/com/metallic/chiaki/session/StreamInput.kt | 5 +++-- .../com/metallic/chiaki/settings/SettingsFragment.kt | 2 ++ android/app/src/main/res/drawable/ic_motion.xml | 9 +++++++++ android/app/src/main/res/values/strings.xml | 3 +++ android/app/src/main/res/xml/preferences.xml | 6 ++++++ 6 files changed, 28 insertions(+), 2 deletions(-) create mode 100644 android/app/src/main/res/drawable/ic_motion.xml diff --git a/android/app/src/main/java/com/metallic/chiaki/common/Preferences.kt b/android/app/src/main/java/com/metallic/chiaki/common/Preferences.kt index d779c5a..2456fcf 100644 --- a/android/app/src/main/java/com/metallic/chiaki/common/Preferences.kt +++ b/android/app/src/main/java/com/metallic/chiaki/common/Preferences.kt @@ -78,6 +78,11 @@ class Preferences(context: Context) get() = sharedPreferences.getBoolean(rumbleEnabledKey, true) set(value) { sharedPreferences.edit().putBoolean(rumbleEnabledKey, value).apply() } + val motionEnabledKey get() = resources.getString(R.string.preferences_motion_enabled_key) + var motionEnabled + get() = sharedPreferences.getBoolean(motionEnabledKey, true) + set(value) { sharedPreferences.edit().putBoolean(motionEnabledKey, value).apply() } + val logVerboseKey get() = resources.getString(R.string.preferences_log_verbose_key) var logVerbose get() = sharedPreferences.getBoolean(logVerboseKey, false) diff --git a/android/app/src/main/java/com/metallic/chiaki/session/StreamInput.kt b/android/app/src/main/java/com/metallic/chiaki/session/StreamInput.kt index 6fbfc31..822fe76 100644 --- a/android/app/src/main/java/com/metallic/chiaki/session/StreamInput.kt +++ b/android/app/src/main/java/com/metallic/chiaki/session/StreamInput.kt @@ -86,7 +86,7 @@ class StreamInput(val context: Context, val preferences: Preferences) override fun onAccuracyChanged(sensor: Sensor, accuracy: Int) {} } - private val lifecycleObserver = object: LifecycleObserver { + private val motionLifecycleObserver = object: LifecycleObserver { @OnLifecycleEvent(Lifecycle.Event.ON_RESUME) fun onResume() { @@ -111,7 +111,8 @@ class StreamInput(val context: Context, val preferences: Preferences) fun observe(lifecycleOwner: LifecycleOwner) { - lifecycleOwner.lifecycle.addObserver(lifecycleObserver) + if(preferences.motionEnabled) + lifecycleOwner.lifecycle.addObserver(motionLifecycleObserver) } private fun controllerStateUpdated() diff --git a/android/app/src/main/java/com/metallic/chiaki/settings/SettingsFragment.kt b/android/app/src/main/java/com/metallic/chiaki/settings/SettingsFragment.kt index 957d1d5..222d130 100644 --- a/android/app/src/main/java/com/metallic/chiaki/settings/SettingsFragment.kt +++ b/android/app/src/main/java/com/metallic/chiaki/settings/SettingsFragment.kt @@ -26,6 +26,7 @@ class DataStore(val preferences: Preferences): PreferenceDataStore() preferences.logVerboseKey -> preferences.logVerbose preferences.swapCrossMoonKey -> preferences.swapCrossMoon preferences.rumbleEnabledKey -> preferences.rumbleEnabled + preferences.motionEnabledKey -> preferences.motionEnabled else -> defValue } @@ -36,6 +37,7 @@ class DataStore(val preferences: Preferences): PreferenceDataStore() preferences.logVerboseKey -> preferences.logVerbose = value preferences.swapCrossMoonKey -> preferences.swapCrossMoon = value preferences.rumbleEnabledKey -> preferences.rumbleEnabled = value + preferences.motionEnabledKey -> preferences.motionEnabled = value } } diff --git a/android/app/src/main/res/drawable/ic_motion.xml b/android/app/src/main/res/drawable/ic_motion.xml new file mode 100644 index 0000000..c5a4ea1 --- /dev/null +++ b/android/app/src/main/res/drawable/ic_motion.xml @@ -0,0 +1,9 @@ + + + diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml index 0f7161d..84a1a59 100644 --- a/android/app/src/main/res/values/strings.xml +++ b/android/app/src/main/res/values/strings.xml @@ -90,6 +90,8 @@ Swap face buttons if default mapping is incorrect (e.g. for 8BitDo controllers) Rumble Use phone vibration motor for rumble + Motion + Use device\'s motion sensors for controller motion Are you sure you want to delete the registered console %s with ID %s? Are you sure you want to delete the console entry for %s? Keep @@ -105,6 +107,7 @@ on_screen_controls_enabled touchpad_only_enabled rumble_enabled + motion_enabled log_verbose import_settings export_settings diff --git a/android/app/src/main/res/xml/preferences.xml b/android/app/src/main/res/xml/preferences.xml index e87afdb..0ec2c0d 100644 --- a/android/app/src/main/res/xml/preferences.xml +++ b/android/app/src/main/res/xml/preferences.xml @@ -25,6 +25,12 @@ app:summary="@string/preferences_rumble_enabled_summary" app:icon="@drawable/ic_rumble" /> + + Date: Fri, 15 Jan 2021 18:43:09 +0100 Subject: [PATCH 189/237] Add Touch Button Haptics to Android --- .../com/metallic/chiaki/common/Preferences.kt | 5 ++++ .../chiaki/settings/SettingsFragment.kt | 2 ++ .../chiaki/touchcontrols/ButtonHaptics.kt | 29 +++++++++++++++++++ .../chiaki/touchcontrols/ButtonView.kt | 4 +++ .../metallic/chiaki/touchcontrols/DPadView.kt | 4 +++ .../chiaki/touchcontrols/TouchpadView.kt | 5 ++++ .../main/res/drawable/ic_button_haptic.xml | 9 ++++++ android/app/src/main/res/values/strings.xml | 3 ++ android/app/src/main/res/xml/preferences.xml | 6 ++++ 9 files changed, 67 insertions(+) create mode 100644 android/app/src/main/java/com/metallic/chiaki/touchcontrols/ButtonHaptics.kt create mode 100644 android/app/src/main/res/drawable/ic_button_haptic.xml diff --git a/android/app/src/main/java/com/metallic/chiaki/common/Preferences.kt b/android/app/src/main/java/com/metallic/chiaki/common/Preferences.kt index 2456fcf..3c4f2f9 100644 --- a/android/app/src/main/java/com/metallic/chiaki/common/Preferences.kt +++ b/android/app/src/main/java/com/metallic/chiaki/common/Preferences.kt @@ -83,6 +83,11 @@ class Preferences(context: Context) get() = sharedPreferences.getBoolean(motionEnabledKey, true) set(value) { sharedPreferences.edit().putBoolean(motionEnabledKey, value).apply() } + val buttonHapticEnabledKey get() = resources.getString(R.string.preferences_button_haptic_enabled_key) + var buttonHapticEnabled + get() = sharedPreferences.getBoolean(buttonHapticEnabledKey, true) + set(value) { sharedPreferences.edit().putBoolean(buttonHapticEnabledKey, value).apply() } + val logVerboseKey get() = resources.getString(R.string.preferences_log_verbose_key) var logVerbose get() = sharedPreferences.getBoolean(logVerboseKey, false) diff --git a/android/app/src/main/java/com/metallic/chiaki/settings/SettingsFragment.kt b/android/app/src/main/java/com/metallic/chiaki/settings/SettingsFragment.kt index 222d130..f574d09 100644 --- a/android/app/src/main/java/com/metallic/chiaki/settings/SettingsFragment.kt +++ b/android/app/src/main/java/com/metallic/chiaki/settings/SettingsFragment.kt @@ -27,6 +27,7 @@ class DataStore(val preferences: Preferences): PreferenceDataStore() preferences.swapCrossMoonKey -> preferences.swapCrossMoon preferences.rumbleEnabledKey -> preferences.rumbleEnabled preferences.motionEnabledKey -> preferences.motionEnabled + preferences.buttonHapticEnabledKey -> preferences.buttonHapticEnabled else -> defValue } @@ -38,6 +39,7 @@ class DataStore(val preferences: Preferences): PreferenceDataStore() preferences.swapCrossMoonKey -> preferences.swapCrossMoon = value preferences.rumbleEnabledKey -> preferences.rumbleEnabled = value preferences.motionEnabledKey -> preferences.motionEnabled = value + preferences.buttonHapticEnabledKey -> preferences.buttonHapticEnabled = value } } diff --git a/android/app/src/main/java/com/metallic/chiaki/touchcontrols/ButtonHaptics.kt b/android/app/src/main/java/com/metallic/chiaki/touchcontrols/ButtonHaptics.kt new file mode 100644 index 0000000..909eab4 --- /dev/null +++ b/android/app/src/main/java/com/metallic/chiaki/touchcontrols/ButtonHaptics.kt @@ -0,0 +1,29 @@ +package com.metallic.chiaki.touchcontrols + +import android.content.Context +import android.os.Build +import android.os.VibrationEffect +import android.os.Vibrator +import androidx.appcompat.app.AppCompatActivity +import com.metallic.chiaki.common.Preferences + +class ButtonHaptics(val context: Context) +{ + private val enabled = Preferences(context).buttonHapticEnabled + + fun trigger(harder: Boolean = false) + { + if(!enabled) + return + val vibrator = context.getSystemService(AppCompatActivity.VIBRATOR_SERVICE) as Vibrator + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) + vibrator.vibrate( + if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) + VibrationEffect.createPredefined(if(harder) VibrationEffect.EFFECT_CLICK else VibrationEffect.EFFECT_TICK) + else + VibrationEffect.createOneShot(10, if(harder) 200 else 100) + ) + else + vibrator.vibrate(10) + } +} \ No newline at end of file diff --git a/android/app/src/main/java/com/metallic/chiaki/touchcontrols/ButtonView.kt b/android/app/src/main/java/com/metallic/chiaki/touchcontrols/ButtonView.kt index e82ffac..078f751 100644 --- a/android/app/src/main/java/com/metallic/chiaki/touchcontrols/ButtonView.kt +++ b/android/app/src/main/java/com/metallic/chiaki/touchcontrols/ButtonView.kt @@ -16,6 +16,8 @@ class ButtonView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 ) : View(context, attrs, defStyleAttr) { + private val haptics = ButtonHaptics(context) + var buttonPressed = false private set(value) { @@ -23,6 +25,8 @@ class ButtonView @JvmOverloads constructor( field = value if(diff) { + if(value) + haptics.trigger() invalidate() buttonPressedCallback?.let { it(field) } } diff --git a/android/app/src/main/java/com/metallic/chiaki/touchcontrols/DPadView.kt b/android/app/src/main/java/com/metallic/chiaki/touchcontrols/DPadView.kt index 0006546..9c359d8 100644 --- a/android/app/src/main/java/com/metallic/chiaki/touchcontrols/DPadView.kt +++ b/android/app/src/main/java/com/metallic/chiaki/touchcontrols/DPadView.kt @@ -16,6 +16,8 @@ class DPadView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 ) : View(context, attrs, defStyleAttr) { + private val haptics = ButtonHaptics(context) + enum class Direction { LEFT, RIGHT, @@ -113,6 +115,8 @@ class DPadView @JvmOverloads constructor( if(state != newState) { + if(newState != null) + haptics.trigger() state = newState invalidate() stateChangeCallback?.let { it(state) } diff --git a/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchpadView.kt b/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchpadView.kt index 9dba03d..a7e7b85 100644 --- a/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchpadView.kt +++ b/android/app/src/main/java/com/metallic/chiaki/touchcontrols/TouchpadView.kt @@ -26,6 +26,8 @@ class TouchpadView @JvmOverloads constructor( private const val BUTTON_HOLD_DELAY_MS = 500L } + private val haptics = ButtonHaptics(context) + private val drawableIdle: Drawable? private val drawablePressed: Drawable? @@ -49,8 +51,10 @@ class TouchpadView @JvmOverloads constructor( val startButtonHoldRunnable = Runnable { if(!moveInsignificant || buttonHeld) return@Runnable + haptics.trigger(true) state.buttons = state.buttons or ControllerState.BUTTON_TOUCHPAD buttonHeld = true + triggerStateChanged() } } private val pointerTouches = mutableMapOf() @@ -105,6 +109,7 @@ class TouchpadView @JvmOverloads constructor( { MotionEvent.ACTION_DOWN, MotionEvent.ACTION_POINTER_DOWN -> { state.startTouch(touchX(event, event.actionIndex), touchY(event, event.actionIndex))?.let { + haptics.trigger() val touch = Touch(it, event.getX(event.actionIndex), event.getY(event.actionIndex)) pointerTouches[event.getPointerId(event.actionIndex)] = touch if(!buttonHeld) diff --git a/android/app/src/main/res/drawable/ic_button_haptic.xml b/android/app/src/main/res/drawable/ic_button_haptic.xml new file mode 100644 index 0000000..fea4c99 --- /dev/null +++ b/android/app/src/main/res/drawable/ic_button_haptic.xml @@ -0,0 +1,9 @@ + + + diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml index 84a1a59..254042b 100644 --- a/android/app/src/main/res/values/strings.xml +++ b/android/app/src/main/res/values/strings.xml @@ -92,6 +92,8 @@ Use phone vibration motor for rumble Motion Use device\'s motion sensors for controller motion + Touch Haptics + Use phone vibration motor for short haptic feedback on button touches Are you sure you want to delete the registered console %s with ID %s? Are you sure you want to delete the console entry for %s? Keep @@ -108,6 +110,7 @@ touchpad_only_enabled rumble_enabled motion_enabled + button_haptic_enabled log_verbose import_settings export_settings diff --git a/android/app/src/main/res/xml/preferences.xml b/android/app/src/main/res/xml/preferences.xml index 0ec2c0d..49989db 100644 --- a/android/app/src/main/res/xml/preferences.xml +++ b/android/app/src/main/res/xml/preferences.xml @@ -31,6 +31,12 @@ app:summary="@string/preferences_motion_enabled_summary" app:icon="@drawable/ic_motion" /> + + Date: Fri, 15 Jan 2021 18:50:49 +0100 Subject: [PATCH 190/237] Fix RegistActivity on Android --- .../src/main/java/com/metallic/chiaki/regist/RegistActivity.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/android/app/src/main/java/com/metallic/chiaki/regist/RegistActivity.kt b/android/app/src/main/java/com/metallic/chiaki/regist/RegistActivity.kt index a8d06f6..9190f9c 100644 --- a/android/app/src/main/java/com/metallic/chiaki/regist/RegistActivity.kt +++ b/android/app/src/main/java/com/metallic/chiaki/regist/RegistActivity.kt @@ -41,7 +41,7 @@ class RegistActivity: AppCompatActivity(), RevealActivity { super.onCreate(savedInstanceState) binding = ActivityRegistBinding.inflate(layoutInflater) - setContentView(R.layout.activity_regist) + setContentView(binding.root) handleReveal() viewModel = ViewModelProvider(this).get(RegistViewModel::class.java) From 35130b08b700591e9a279f53e8380e69ca4eca0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Fri, 15 Jan 2021 19:16:54 +0100 Subject: [PATCH 191/237] Refine LR Touch on Android --- .../chiaki/touchcontrols/ButtonView.kt | 11 ++++++---- .../src/main/res/layout/fragment_controls.xml | 20 ++++++++++++------- 2 files changed, 20 insertions(+), 11 deletions(-) diff --git a/android/app/src/main/java/com/metallic/chiaki/touchcontrols/ButtonView.kt b/android/app/src/main/java/com/metallic/chiaki/touchcontrols/ButtonView.kt index 078f751..66a9afe 100644 --- a/android/app/src/main/java/com/metallic/chiaki/touchcontrols/ButtonView.kt +++ b/android/app/src/main/java/com/metallic/chiaki/touchcontrols/ButtonView.kt @@ -6,6 +6,7 @@ import android.content.Context import android.graphics.Canvas import android.graphics.drawable.Drawable import android.util.AttributeSet +import android.util.Log import android.view.MotionEvent import android.view.View import android.view.ViewGroup @@ -75,14 +76,16 @@ class ButtonView @JvmOverloads constructor( override fun onTouchEvent(event: MotionEvent): Boolean { - when(event.action) + when(event.actionMasked) { - MotionEvent.ACTION_DOWN -> { - if(bestFittingTouchView(event.x, event.y) != this) + MotionEvent.ACTION_DOWN, MotionEvent.ACTION_POINTER_DOWN -> { + if(bestFittingTouchView(event.getX(event.actionIndex), event.getY(event.actionIndex)) != this) return false buttonPressed = true } - MotionEvent.ACTION_UP -> buttonPressed = false + MotionEvent.ACTION_UP, MotionEvent.ACTION_POINTER_UP -> { + buttonPressed = false + } } return true } diff --git a/android/app/src/main/res/layout/fragment_controls.xml b/android/app/src/main/res/layout/fragment_controls.xml index 244956a..7a6b3af 100644 --- a/android/app/src/main/res/layout/fragment_controls.xml +++ b/android/app/src/main/res/layout/fragment_controls.xml @@ -173,9 +173,12 @@ Date: Fri, 15 Jan 2021 21:48:59 +0100 Subject: [PATCH 192/237] Fix FindFFMPEG.cmake for ancient cmake --- cmake/FindFFMPEG.cmake | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/cmake/FindFFMPEG.cmake b/cmake/FindFFMPEG.cmake index 32e8ac0..bdb66aa 100644 --- a/cmake/FindFFMPEG.cmake +++ b/cmake/FindFFMPEG.cmake @@ -49,7 +49,11 @@ function (_ffmpeg_find component headername) # Try pkg-config first if(PKG_CONFIG_FOUND) - pkg_check_modules(FFMPEG_${component} lib${component} IMPORTED_TARGET) + if(CMAKE_VERSION VERSION_LESS "3.6") + pkg_check_modules(FFMPEG_${component} lib${component}) + else() + pkg_check_modules(FFMPEG_${component} lib${component} IMPORTED_TARGET) + endif() if(FFMPEG_${component}_FOUND) if((TARGET PkgConfig::FFMPEG_${component}) AND (NOT CMAKE_VERSION VERSION_LESS "3.11.0")) if(APPLE) @@ -69,6 +73,9 @@ function (_ffmpeg_find component headername) add_library(FFMPEG::${component} ALIAS PkgConfig::FFMPEG_${component}) else() add_library("FFMPEG::${component}" INTERFACE IMPORTED) + if(CMAKE_VERSION VERSION_LESS "3.6") + link_directories("${FFMPEG_${component}_LIBRARY_DIRS}") + endif() set_target_properties("FFMPEG::${component}" PROPERTIES INTERFACE_LINK_DIRECTORIES "${FFMPEG_${component}_LIBRARY_DIRS}" INTERFACE_INCLUDE_DIRECTORIES "${FFMPEG_${component}_INCLUDE_DIRS}" @@ -224,10 +231,14 @@ foreach (_ffmpeg_component IN LISTS FFMPEG_FIND_COMPONENTS) list(APPEND _ffmpeg_required_vars "FFMPEG_${_ffmpeg_component}_LIBRARIES") else() - set(FFMPEG_${_ffmpeg_component}_INCLUDE_DIRS - "${FFMPEG_${_ffmpeg_component}_INCLUDE_DIR}") - set(FFMPEG_${_ffmpeg_component}_LIBRARIES - "${FFMPEG_${_ffmpeg_component}_LIBRARY}") + if(NOT FFMPEG_${_ffmpeg_component}_INCLUDE_DIRS) + set(FFMPEG_${_ffmpeg_component}_INCLUDE_DIRS + "${FFMPEG_${_ffmpeg_component}_INCLUDE_DIR}") + endif() + if(NOT FFMPEG_${_ffmpeg_component}_LIBRARIES) + set(FFMPEG_${_ffmpeg_component}_LIBRARIES + "${FFMPEG_${_ffmpeg_component}_LIBRARY}") + endif() list(APPEND FFMPEG_INCLUDE_DIRS "${FFMPEG_${_ffmpeg_component}_INCLUDE_DIRS}") list(APPEND FFMPEG_LIBRARIES From 505910bc5fc611183ce0831cf8f92936de99827e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Fri, 15 Jan 2021 21:14:37 +0100 Subject: [PATCH 193/237] Enable Setsu in AppImage --- scripts/Dockerfile.xenial | 4 ++-- scripts/build-appimage.sh | 2 ++ scripts/build-ffmpeg.sh | 2 +- setsu/cmake/FindEvdev.cmake | 6 +++++- setsu/cmake/FindUdev.cmake | 6 +++++- 5 files changed, 15 insertions(+), 5 deletions(-) diff --git a/scripts/Dockerfile.xenial b/scripts/Dockerfile.xenial index b741028..dd92581 100644 --- a/scripts/Dockerfile.xenial +++ b/scripts/Dockerfile.xenial @@ -5,9 +5,9 @@ RUN apt-get update RUN apt-get install -y software-properties-common RUN add-apt-repository ppa:beineri/opt-qt-5.12.3-xenial RUN apt-get update -RUN apt-get -y install git g++ cmake ninja-build curl unzip python3-pip \ +RUN apt-get -y install git g++ cmake ninja-build curl pkg-config unzip python3-pip \ libssl-dev libopus-dev qt512base qt512multimedia qt512svg \ - libgl1-mesa-dev nasm libudev-dev libva-dev fuse + libgl1-mesa-dev nasm libudev-dev libva-dev fuse libevdev-dev libudev-dev CMD [] diff --git a/scripts/build-appimage.sh b/scripts/build-appimage.sh index 7ba4acf..dd01ef4 100755 --- a/scripts/build-appimage.sh +++ b/scripts/build-appimage.sh @@ -18,6 +18,8 @@ cmake \ "-DCMAKE_PREFIX_PATH=`pwd`/../appimage/ffmpeg-prefix;`pwd`/../appimage/sdl2-prefix;/opt/qt512" \ -DCHIAKI_ENABLE_TESTS=ON \ -DCHIAKI_ENABLE_CLI=OFF \ + -DCHIAKI_ENABLE_GUI=ON \ + -DCHIAKI_ENABLE_SETSU=ON \ -DCHIAKI_GUI_ENABLE_SDL_GAMECONTROLLER=ON \ -DCMAKE_INSTALL_PREFIX=/usr \ .. diff --git a/scripts/build-ffmpeg.sh b/scripts/build-ffmpeg.sh index 715a87f..d00b962 100755 --- a/scripts/build-ffmpeg.sh +++ b/scripts/build-ffmpeg.sh @@ -9,6 +9,6 @@ TAG=n4.3.1 git clone https://git.ffmpeg.org/ffmpeg.git --depth 1 -b $TAG && cd ffmpeg || exit 1 -./configure --disable-all --enable-avcodec --enable-decoder=h264 --enable-decoder=hevc --enable-hwaccel=h264_vaapi --enable-hwaccel=hevc_vaapi --prefix="$ROOT/ffmpeg-prefix" "$@" || exit 1 +./configure --disable-all --enable-avcodec --enable-decoder=h264 --enable-decoder=hevc --enable-hwaccel=h264_vaapi --enable-hwaccel=hevc_vaapi --prefix="$ROOT/ffmpeg-prefix" "$@" || exit 1 make -j4 || exit 1 make install || exit 1 diff --git a/setsu/cmake/FindEvdev.cmake b/setsu/cmake/FindEvdev.cmake index 5ea7dea..23f2515 100644 --- a/setsu/cmake/FindEvdev.cmake +++ b/setsu/cmake/FindEvdev.cmake @@ -5,7 +5,11 @@ set(_target "${_prefix}::libevdev") find_package(PkgConfig) if(PkgConfig_FOUND AND NOT TARGET ${_target}) - pkg_check_modules("${_prefix}" libevdev IMPORTED_TARGET) + if(CMAKE_VERSION VERSION_LESS "3.6") + pkg_check_modules("${_prefix}" libevdev) + else() + pkg_check_modules("${_prefix}" libevdev IMPORTED_TARGET) + endif() if((TARGET PkgConfig::${_prefix}) AND (NOT CMAKE_VERSION VERSION_LESS "3.11.0")) set_target_properties(PkgConfig::${_prefix} PROPERTIES IMPORTED_GLOBAL ON) add_library(${_target} ALIAS PkgConfig::${_prefix}) diff --git a/setsu/cmake/FindUdev.cmake b/setsu/cmake/FindUdev.cmake index fc1c81f..c9c8450 100644 --- a/setsu/cmake/FindUdev.cmake +++ b/setsu/cmake/FindUdev.cmake @@ -5,7 +5,11 @@ set(_target "${_prefix}::libudev") find_package(PkgConfig) if(PkgConfig_FOUND AND NOT TARGET ${_target}) - pkg_check_modules("${_prefix}" libudev IMPORTED_TARGET) + if(CMAKE_VERSION VERSION_LESS "3.6") + pkg_check_modules("${_prefix}" libudev) + else() + pkg_check_modules("${_prefix}" libudev IMPORTED_TARGET) + endif() if((TARGET PkgConfig::${_prefix}) AND (NOT CMAKE_VERSION VERSION_LESS "3.11.0")) set_target_properties(PkgConfig::${_prefix} PROPERTIES IMPORTED_GLOBAL ON) add_library(${_target} ALIAS PkgConfig::${_prefix}) From fcdc414692b33ecae1f30a19dc4cd81d4bd77121 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Fri, 15 Jan 2021 22:35:50 +0100 Subject: [PATCH 194/237] Version 2.1.0 --- CMakeLists.txt | 4 ++-- README.md | 18 ++++++++---------- android/app/build.gradle | 2 +- 3 files changed, 11 insertions(+), 13 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index a6beae8..02a1484 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -31,8 +31,8 @@ tri_option(CHIAKI_USE_SYSTEM_JERASURE "Use system-provided jerasure instead of s tri_option(CHIAKI_USE_SYSTEM_NANOPB "Use system-provided nanopb instead of submodule" AUTO) set(CHIAKI_VERSION_MAJOR 2) -set(CHIAKI_VERSION_MINOR 0) -set(CHIAKI_VERSION_PATCH 1) +set(CHIAKI_VERSION_MINOR 1) +set(CHIAKI_VERSION_PATCH 0) set(CHIAKI_VERSION ${CHIAKI_VERSION_MAJOR}.${CHIAKI_VERSION_MINOR}.${CHIAKI_VERSION_PATCH}) set(CPACK_PACKAGE_NAME "chiaki") diff --git a/README.md b/README.md index 5129f3a..f306470 100644 --- a/README.md +++ b/README.md @@ -12,13 +12,18 @@ for Linux, FreeBSD, OpenBSD, Android, macOS, Windows, Nintendo Switch and potent ![Screenshot](assets/screenshot.png) +## Project Status + +As all relevant features are implemented, this project is considered to be finished and in maintenance mode only. +No major updates are planned and contributions are only accepted in special cases. + ## Installing You can either download a pre-built release or build Chiaki from source. ### Downloading a Release -Builds are provided for Linux, Android, macOS and Windows. +Builds are provided for Linux, Android, macOS, Nintendo Switch and Windows. You can download them [here](https://git.sr.ht/~thestr4ng3r/chiaki/refs). @@ -26,7 +31,7 @@ You can download them [here](https://git.sr.ht/~thestr4ng3r/chiaki/refs). * **Android**: Install from [Google Play](https://play.google.com/store/apps/details?id=com.metallic.chiaki), [F-Droid](https://f-droid.org/packages/com.metallic.chiaki/) or download the APK from Sourcehut. * **macOS**: Drag the application from the `.dmg` into your Applications folder. * **Windows**: Extract the `.zip` file and execute `chiaki.exe`. -* **Switch**: Follow README specific [instructions](./switch/README.md) +* **Switch**: Download the `.nro` file and copy it into the `switch/` directory on your SD card. ### Building from Source @@ -39,7 +44,7 @@ cmake .. make ``` -For more detailed platform-specific instructions, see [doc/platform-build.md](doc/platform-build.md). +For more detailed platform-specific instructions, see [doc/platform-build.md](doc/platform-build.md) or [switch/](./switch/README.md) for Nintendo Switch. ## Usage @@ -63,13 +68,6 @@ Settings -> Remote Play -> Add Device, or on a PS5: Settings -> System -> Remote You can now double-click your Console in Chiaki's main window to start Remote Play. -## Joining the Community or Getting Help - -There are official groups for Chiaki on Telegram and IRC. They are bridged so you can join whichever you like: - -- **Telegram:** https://t.me/chiakitg -- **IRC:** #chiaki on irc.freenode.net - ## Acknowledgements This project has only been made possible because of the following Open Source projects: diff --git a/android/app/build.gradle b/android/app/build.gradle index 48bf54c..e87200c 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -24,7 +24,7 @@ android { applicationId "com.metallic.chiaki" minSdkVersion 21 targetSdkVersion 30 - versionCode 9 + versionCode 10 versionName chiakiVersion externalNativeBuild { cmake { From ac8a3a3a3c9428800de35a9a2bd00332da202b5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Fri, 15 Jan 2021 22:49:56 +0100 Subject: [PATCH 195/237] Update Flatpak to v2.1.0 --- scripts/flatpak/com.github.thestr4ng3r.Chiaki.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/flatpak/com.github.thestr4ng3r.Chiaki.json b/scripts/flatpak/com.github.thestr4ng3r.Chiaki.json index 3a03107..992ca18 100644 --- a/scripts/flatpak/com.github.thestr4ng3r.Chiaki.json +++ b/scripts/flatpak/com.github.thestr4ng3r.Chiaki.json @@ -89,8 +89,8 @@ { "type": "git", "url": "https://git.sr.ht/~thestr4ng3r/chiaki", - "tag": "v2.0.1", - "commit": "9e698dd7c4e4011ff6e136741abef5cf4b32527c" + "tag": "v2.1.0", + "commit": "fcdc414692b33ecae1f30a19dc4cd81d4bd77121" } ] } From 078e83ec70b3d008df8a4c1249e10457b46a1096 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Sun, 24 Jan 2021 10:11:46 +0100 Subject: [PATCH 196/237] Fix Regist on Switch --- switch/src/host.cpp | 1 + switch/src/settings.cpp | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/switch/src/host.cpp b/switch/src/host.cpp index 2f50e2f..39ac1b3 100644 --- a/switch/src/host.cpp +++ b/switch/src/host.cpp @@ -117,6 +117,7 @@ int Host::Register(int pin) throw Exception("Undefined PS4 system version (please run discover first)"); } + this->regist_info.pin = pin; this->regist_info.host = this->host_addr.c_str(); this->regist_info.broadcast = false; diff --git a/switch/src/settings.cpp b/switch/src/settings.cpp index 8cb5264..fb39861 100644 --- a/switch/src/settings.cpp +++ b/switch/src/settings.cpp @@ -7,9 +7,9 @@ Settings::Settings() { #if defined(__SWITCH__) - chiaki_log_init(&this->log, CHIAKI_LOG_ALL ^ CHIAKI_LOG_VERBOSE ^ CHIAKI_LOG_DEBUG, chiaki_log_cb_print, NULL); + chiaki_log_init(&this->log, CHIAKI_LOG_ALL & ~(CHIAKI_LOG_VERBOSE | CHIAKI_LOG_DEBUG), chiaki_log_cb_print, NULL); #else - chiaki_log_init(&this->log, CHIAKI_LOG_ALL, chiaki_log_cb_print, NULL); + chiaki_log_init(&this->log, CHIAKI_LOG_ALL & ~CHIAKI_LOG_VERBOSE, chiaki_log_cb_print, NULL); #endif } From 6df937a57ca63093e973af58de8776a6b4be607a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Sun, 24 Jan 2021 10:17:49 +0100 Subject: [PATCH 197/237] Remove removed sdl2-static from Alpine --- .builds/common.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.builds/common.yml b/.builds/common.yml index 46b754f..6208cad 100644 --- a/.builds/common.yml +++ b/.builds/common.yml @@ -15,7 +15,6 @@ packages: - qt5-qtmultimedia-dev - ffmpeg-dev - sdl2-dev - - sdl2-static # this is gone on alpine edge so might be necessary to remove later - docker - fuse From 2257030adeb5c5a84380c78d34be4d783971663c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Sun, 24 Jan 2021 10:13:29 +0100 Subject: [PATCH 198/237] Version 2.1.1 --- CMakeLists.txt | 2 +- android/app/build.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 02a1484..611af18 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -32,7 +32,7 @@ tri_option(CHIAKI_USE_SYSTEM_NANOPB "Use system-provided nanopb instead of submo set(CHIAKI_VERSION_MAJOR 2) set(CHIAKI_VERSION_MINOR 1) -set(CHIAKI_VERSION_PATCH 0) +set(CHIAKI_VERSION_PATCH 1) set(CHIAKI_VERSION ${CHIAKI_VERSION_MAJOR}.${CHIAKI_VERSION_MINOR}.${CHIAKI_VERSION_PATCH}) set(CPACK_PACKAGE_NAME "chiaki") diff --git a/android/app/build.gradle b/android/app/build.gradle index e87200c..282b42c 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -24,7 +24,7 @@ android { applicationId "com.metallic.chiaki" minSdkVersion 21 targetSdkVersion 30 - versionCode 10 + versionCode 11 versionName chiakiVersion externalNativeBuild { cmake { From ae3ca1ac7a7cbdd6585a3c56ff72fdffba8809ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Sun, 24 Jan 2021 14:02:17 +0100 Subject: [PATCH 199/237] Update Flatpak to v2.1.1 --- scripts/flatpak/com.github.thestr4ng3r.Chiaki.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/flatpak/com.github.thestr4ng3r.Chiaki.json b/scripts/flatpak/com.github.thestr4ng3r.Chiaki.json index 992ca18..daab8a0 100644 --- a/scripts/flatpak/com.github.thestr4ng3r.Chiaki.json +++ b/scripts/flatpak/com.github.thestr4ng3r.Chiaki.json @@ -89,8 +89,8 @@ { "type": "git", "url": "https://git.sr.ht/~thestr4ng3r/chiaki", - "tag": "v2.1.0", - "commit": "fcdc414692b33ecae1f30a19dc4cd81d4bd77121" + "tag": "v2.1.1", + "commit": "2257030adeb5c5a84380c78d34be4d783971663c" } ] } From a049ed43ec5e0316dee2edd718757ce643e3cd1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Tue, 2 Feb 2021 10:59:26 +0100 Subject: [PATCH 200/237] Force-disable Lib Decoders on Android --- android/app/build.gradle | 2 ++ 1 file changed, 2 insertions(+) diff --git a/android/app/build.gradle b/android/app/build.gradle index 282b42c..87b8761 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -33,6 +33,8 @@ android { "-DCHIAKI_ENABLE_GUI=OFF", "-DCHIAKI_ENABLE_SETSU=OFF", "-DCHIAKI_ENABLE_ANDROID=ON", + "-DCHIAKI_ENABLE_FFMPEG_DECODER=OFF", + "-DCHIAKI_ENABLE_PI_DECODER=OFF", "-DCHIAKI_LIB_ENABLE_OPUS=OFF", "-DCHIAKI_LIB_OPENSSL_EXTERNAL_PROJECT=ON" } From f50b060795e4648eefc9efd431f0137adbd94a1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Sun, 11 Apr 2021 17:24:30 +0200 Subject: [PATCH 201/237] Fix AppVeyor --- .appveyor.yml | 6 +++--- scripts/appveyor-win.sh | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.appveyor.yml b/.appveyor.yml index 933f517..2731ccb 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -36,14 +36,14 @@ for: install: - git submodule update --init --recursive - sudo pip3 install protobuf - - brew install qt opus openssl@1.1 nasm sdl2 protobuf + - brew install qt@5 opus openssl@1.1 nasm sdl2 protobuf - scripts/build-ffmpeg.sh build_script: - - export CMAKE_PREFIX_PATH="`pwd`/ffmpeg-prefix;/usr/local/opt/openssl@1.1;/usr/local/opt/qt" + - export CMAKE_PREFIX_PATH="`pwd`/ffmpeg-prefix;/usr/local/opt/openssl@1.1;/usr/local/opt/qt@5" - scripts/build-common.sh - cp -a build/gui/chiaki.app Chiaki.app - - /usr/local/opt/qt/bin/macdeployqt Chiaki.app -dmg + - /usr/local/opt/qt@5/bin/macdeployqt Chiaki.app -dmg artifacts: - path: Chiaki.dmg diff --git a/scripts/appveyor-win.sh b/scripts/appveyor-win.sh index f751200..585eb2c 100755 --- a/scripts/appveyor-win.sh +++ b/scripts/appveyor-win.sh @@ -26,7 +26,7 @@ ninja || exit 1 ninja install || exit 1 cd ../.. || exit 1 -wget https://mirror.firedaemon.com/OpenSSL/openssl-1.1.1d-dev.zip && 7z x openssl-1.1.1d-dev.zip || exit 1 +wget https://mirror.firedaemon.com/OpenSSL/openssl-1.1.1l-dev.zip && 7z x openssl-1.1.1l-dev.zip || exit 1 wget https://www.libsdl.org/release/SDL2-devel-2.0.10-VC.zip && 7z x SDL2-devel-2.0.10-VC.zip || exit 1 export SDL_ROOT="$APPVEYOR_BUILD_FOLDER/SDL2-2.0.10" || exit 1 From 2b4a7426ff0ec96f89a311bed433826d59f879f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Sun, 11 Apr 2021 14:57:21 +0200 Subject: [PATCH 202/237] Install CLI --- cli/CMakeLists.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/cli/CMakeLists.txt b/cli/CMakeLists.txt index 83c37a9..83de1f9 100644 --- a/cli/CMakeLists.txt +++ b/cli/CMakeLists.txt @@ -15,3 +15,4 @@ endif() add_executable(chiaki-cli src/main.c) target_link_libraries(chiaki-cli chiaki-cli-lib) +install(TARGETS chiaki-cli) From a44000ea2b08a4bdfea397c2a3d493fd623dbcd3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Sun, 11 Apr 2021 15:11:56 +0200 Subject: [PATCH 203/237] Enable CLI in CI --- .builds/common.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.builds/common.yml b/.builds/common.yml index 6208cad..8c7d43d 100644 --- a/.builds/common.yml +++ b/.builds/common.yml @@ -17,6 +17,7 @@ packages: - sdl2-dev - docker - fuse + - argp-standalone artifacts: - chiaki.nro @@ -29,7 +30,7 @@ tasks: sudo service fuse start # Fuse for AppImages - local_build_and_test: | cd chiaki - cmake -Bbuild -GNinja + cmake -Bbuild -GNinja -DCHIAKI_ENABLE_CLI=ON -DCHIAKI_ENABLE_GUI=ON -DCHIAKI_CLI_ARGP_STANDALONE=ON ninja -C build build/test/chiaki-unit - appimage: | From 7870a28cdd5e42e36e10a55f27b05fc2bdb22562 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Sun, 11 Apr 2021 17:07:35 +0200 Subject: [PATCH 204/237] Fix Discovery in CLI --- cli/src/discover.c | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/cli/src/discover.c b/cli/src/discover.c index 503b53a..cf9319f 100644 --- a/cli/src/discover.c +++ b/cli/src/discover.c @@ -142,13 +142,19 @@ CHIAKI_EXPORT int chiaki_cli_cmd_discover(ChiakiLog *log, int argc, char *argv[] return 1; } - ((struct sockaddr_in *)host_addr)->sin_port = htons(CHIAKI_DISCOVERY_PORT_PS4); // TODO: IPv6, PS5, should probably use the service - ChiakiDiscoveryPacket packet; memset(&packet, 0, sizeof(packet)); packet.cmd = CHIAKI_DISCOVERY_CMD_SRCH; - - chiaki_discovery_send(&discovery, &packet, host_addr, host_addr_len); + packet.protocol_version = CHIAKI_DISCOVERY_PROTOCOL_VERSION_PS4; + ((struct sockaddr_in *)host_addr)->sin_port = htons(CHIAKI_DISCOVERY_PORT_PS4); + err = chiaki_discovery_send(&discovery, &packet, host_addr, host_addr_len); + if(err != CHIAKI_ERR_SUCCESS) + CHIAKI_LOGE(log, "Failed to send discovery packet for PS4: %s", chiaki_error_string(err)); + packet.protocol_version = CHIAKI_DISCOVERY_PROTOCOL_VERSION_PS5; + ((struct sockaddr_in *)host_addr)->sin_port = htons(CHIAKI_DISCOVERY_PORT_PS5); + err = chiaki_discovery_send(&discovery, &packet, host_addr, host_addr_len); + if(err != CHIAKI_ERR_SUCCESS) + CHIAKI_LOGE(log, "Failed to send discovery packet for PS5: %s", chiaki_error_string(err)); while(1) sleep(1); // TODO: wtf From 695da184733eace6bc97d6b4fef02f12483a533a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Sun, 11 Apr 2021 17:13:26 +0200 Subject: [PATCH 205/237] Make CLI Wakeup work for PS5 --- cli/src/wakeup.c | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/cli/src/wakeup.c b/cli/src/wakeup.c index cf5ab45..cea2792 100644 --- a/cli/src/wakeup.c +++ b/cli/src/wakeup.c @@ -11,10 +11,14 @@ static char doc[] = "Send a PS4 wakeup packet."; #define ARG_KEY_HOST 'h' #define ARG_KEY_REGISTKEY 'r' +#define ARG_KEY_PS4 '4' +#define ARG_KEY_PS5 '5' static struct argp_option options[] = { { "host", ARG_KEY_HOST, "Host", 0, "Host to send wakeup packet to", 0 }, - { "registkey", ARG_KEY_REGISTKEY, "RegistKey", 0, "PS4 registration key", 0 }, + { "registkey", ARG_KEY_REGISTKEY, "RegistKey", 0, "Remote Play registration key (plaintext)", 0 }, + { "ps4", ARG_KEY_PS4, NULL, 0, "PlayStation 4", 0 }, + { "ps5", ARG_KEY_PS5, NULL, 0, "PlayStation 5 (default)", 0 }, { 0 } }; @@ -22,6 +26,7 @@ typedef struct arguments { const char *host; const char *registkey; + bool ps5; } Arguments; static int parse_opt(int key, char *arg, struct argp_state *state) @@ -39,6 +44,12 @@ static int parse_opt(int key, char *arg, struct argp_state *state) case ARGP_KEY_ARG: argp_usage(state); break; + case ARG_KEY_PS4: + arguments->ps5 = false; + break; + case ARG_KEY_PS5: + arguments->ps5 = true; + break; default: return ARGP_ERR_UNKNOWN; } @@ -51,6 +62,7 @@ static struct argp argp = { options, parse_opt, 0, doc, 0, 0, 0 }; CHIAKI_EXPORT int chiaki_cli_cmd_wakeup(ChiakiLog *log, int argc, char *argv[]) { Arguments arguments = { 0 }; + arguments.ps5 = true; error_t argp_r = argp_parse(&argp, argc, argv, ARGP_IN_ORDER, NULL, &arguments); if(argp_r != 0) return 1; @@ -73,5 +85,5 @@ CHIAKI_EXPORT int chiaki_cli_cmd_wakeup(ChiakiLog *log, int argc, char *argv[]) uint64_t credential = (uint64_t)strtoull(arguments.registkey, NULL, 16); - return chiaki_discovery_wakeup(log, NULL, arguments.host, credential, false); + return chiaki_discovery_wakeup(log, NULL, arguments.host, credential, arguments.ps5); } From 796a12845684afe5c4b194d4111bcdcf5b04aca2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Sun, 11 Apr 2021 18:19:46 +0200 Subject: [PATCH 206/237] Fix fec.c extension --- lib/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/CMakeLists.txt b/lib/CMakeLists.txt index 36ac38c..3eb6e27 100644 --- a/lib/CMakeLists.txt +++ b/lib/CMakeLists.txt @@ -72,7 +72,7 @@ set(SOURCE_FILES src/controller.c src/takionsendbuffer.c src/time.c - src/fec + src/fec.c src/regist.c src/opusdecoder.c src/orientation.c) From 7a01ac0d411b0ec5b74b3a95726025ee88d450fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Fri, 3 Dec 2021 11:46:47 +0100 Subject: [PATCH 207/237] Update BSD CI --- .builds/common.yml | 1 + .builds/freebsd.yml | 4 ++-- .builds/openbsd.yml | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.builds/common.yml b/.builds/common.yml index 8c7d43d..b9ff1b7 100644 --- a/.builds/common.yml +++ b/.builds/common.yml @@ -9,6 +9,7 @@ packages: - ninja - protoc - py3-protobuf + - py3-setuptools - opus-dev - qt5-qtbase-dev - qt5-qtsvg-dev diff --git a/.builds/freebsd.yml b/.builds/freebsd.yml index caa9016..a105aec 100644 --- a/.builds/freebsd.yml +++ b/.builds/freebsd.yml @@ -1,5 +1,5 @@ -image: freebsd/latest +image: freebsd/13.x sources: - https://git.sr.ht/~thestr4ng3r/chiaki @@ -7,7 +7,7 @@ sources: packages: - cmake - protobuf - - py37-protobuf + - py38-protobuf - opus - qt5-core - qt5-qmake diff --git a/.builds/openbsd.yml b/.builds/openbsd.yml index b8785ef..2df5353 100644 --- a/.builds/openbsd.yml +++ b/.builds/openbsd.yml @@ -1,5 +1,5 @@ -image: openbsd/6.7 +image: openbsd/7.0 sources: - https://git.sr.ht/~thestr4ng3r/chiaki From dcd2e6af4a2403f68ad76774525f4d433bb43716 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Fri, 3 Dec 2021 11:47:32 +0100 Subject: [PATCH 208/237] Update nanopb to fix unaligned pointers on arm64 --- third-party/nanopb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/third-party/nanopb b/third-party/nanopb index ae9901f..ab19ecb 160000 --- a/third-party/nanopb +++ b/third-party/nanopb @@ -1 +1 @@ -Subproject commit ae9901f2a31500e8fdc93fa9804d24851c58bb1e +Subproject commit ab19ecbe1b9f377ab4ee8e762bfe16c39068ad68 From ccecc67d74f35b0be41073c2a3ccd237ed955402 Mon Sep 17 00:00:00 2001 From: MiniMeOSc Date: Mon, 21 Mar 2022 22:43:08 +0100 Subject: [PATCH 209/237] Fix stream command not working for second or later host in configuration --- gui/src/main.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/gui/src/main.cpp b/gui/src/main.cpp index 010e744..a96f007 100644 --- a/gui/src/main.cpp +++ b/gui/src/main.cpp @@ -129,15 +129,21 @@ int real_main(int argc, char *argv[]) { if(args.length() < 3) parser.showHelp(1); + + bool found = false; for(const auto &temphost : settings.GetRegisteredHosts()) { if(temphost.GetServerNickname() == args[1]) { + found = true; morning = temphost.GetRPKey(); regist_key = temphost.GetRPRegistKey(); target = temphost.GetTarget(); break; } + } + if(!found) + { printf("No configuration found for '%s'\n", args[1].toLocal8Bit().constData()); return 1; } From aa3a3b8bbcfd1c45a0b10ec6d6d2aa4e3d8bb131 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Sat, 26 Mar 2022 09:53:38 +0100 Subject: [PATCH 210/237] Update Windows Dependencies in CI --- scripts/appveyor-win.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/appveyor-win.sh b/scripts/appveyor-win.sh index 585eb2c..1361114 100755 --- a/scripts/appveyor-win.sh +++ b/scripts/appveyor-win.sh @@ -26,7 +26,7 @@ ninja || exit 1 ninja install || exit 1 cd ../.. || exit 1 -wget https://mirror.firedaemon.com/OpenSSL/openssl-1.1.1l-dev.zip && 7z x openssl-1.1.1l-dev.zip || exit 1 +wget https://mirror.firedaemon.com/OpenSSL/openssl-1.1.1n.zip && 7z x openssl-1.1.1n.zip || exit 1 wget https://www.libsdl.org/release/SDL2-devel-2.0.10-VC.zip && 7z x SDL2-devel-2.0.10-VC.zip || exit 1 export SDL_ROOT="$APPVEYOR_BUILD_FOLDER/SDL2-2.0.10" || exit 1 @@ -43,7 +43,7 @@ export PATH="$PWD/protoc/bin:$PATH" || exit 1 PYTHON="C:/Python37/python.exe" "$PYTHON" -m pip install protobuf || exit 1 -QT_PATH="C:/Qt/5.12/msvc2017_64" +QT_PATH="C:/Qt/5.15/msvc2019_64" COPY_DLLS="$PWD/openssl-1.1/x64/bin/libcrypto-1_1-x64.dll $PWD/openssl-1.1/x64/bin/libssl-1_1-x64.dll $SDL_ROOT/lib/x64/SDL2.dll" From 7d820bd4ab9fa3bc8614b4dff558b55a3db577a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Sat, 26 Mar 2022 13:44:50 +0100 Subject: [PATCH 211/237] Update and fix CI * appimage: switched to bionic * android: updated container for nanopb changes * switch: updated devkitpro and simpler cmake scripts --- .builds/android.yml | 2 +- .builds/common.yml | 2 +- .gitmodules | 2 +- cmake/switch.cmake | 26 ++------- scripts/Dockerfile.bionic | 16 ++++++ scripts/Dockerfile.xenial | 13 ----- scripts/kitware-archive-latest.asc | 64 +++++++++++++++++++++++ scripts/run-docker-build-appimage.sh | 4 +- scripts/switch/build.sh | 3 -- scripts/switch/run-docker-build-chiaki.sh | 2 +- switch/borealis | 2 +- 11 files changed, 90 insertions(+), 46 deletions(-) create mode 100644 scripts/Dockerfile.bionic delete mode 100644 scripts/Dockerfile.xenial create mode 100644 scripts/kitware-archive-latest.asc diff --git a/.builds/android.yml b/.builds/android.yml index 7b64ab4..5e4c259 100644 --- a/.builds/android.yml +++ b/.builds/android.yml @@ -22,7 +22,7 @@ tasks: sudo docker run \ -v /home/build:/home/build \ -u $(id -u):$(id -g) \ - thestr4ng3r/android:f064ea6 \ + thestr4ng3r/android:b2853cc \ /bin/bash -c "cd /home/build/chiaki/android && ./gradlew assembleRelease bundleRelease" cp chiaki/android/app/build/outputs/apk/release/app-release*.apk Chiaki.apk cp chiaki/android/app/build/outputs/bundle/release/app-release*.aab Chiaki.aab diff --git a/.builds/common.yml b/.builds/common.yml index b9ff1b7..4681f14 100644 --- a/.builds/common.yml +++ b/.builds/common.yml @@ -1,5 +1,5 @@ -image: alpine/latest +image: alpine/edge # on edge for https://gitlab.alpinelinux.org/alpine/aports/-/issues/13287 sources: - https://git.sr.ht/~thestr4ng3r/chiaki diff --git a/.gitmodules b/.gitmodules index ac87125..250f8fe 100644 --- a/.gitmodules +++ b/.gitmodules @@ -15,4 +15,4 @@ url = https://github.com/google/oboe [submodule "switch/borealis"] path = switch/borealis - url = https://github.com/natinusala/borealis.git + url = https://git.sr.ht/~thestr4ng3r/borealis diff --git a/cmake/switch.cmake b/cmake/switch.cmake index 6046d67..7e600be 100644 --- a/cmake/switch.cmake +++ b/cmake/switch.cmake @@ -1,34 +1,15 @@ # Find DEVKITPRO set(DEVKITPRO "$ENV{DEVKITPRO}" CACHE PATH "Path to DevKitPro") -set(PORTLIBS_PREFIX "$ENV{PORTLIBS_PREFIX}" CACHE PATH "Path to portlibs inside DevKitPro") -if(NOT DEVKITPRO OR NOT PORTLIBS_PREFIX) - message(FATAL_ERROR "Please set DEVKITPRO & PORTLIBS_PREFIX env before calling cmake. https://devkitpro.org/wiki/Getting_Started") +if(NOT DEVKITPRO) + message(FATAL_ERROR "Please set DEVKITPRO env before calling cmake. https://devkitpro.org/wiki/Getting_Started") endif() # include devkitpro toolchain -include("${DEVKITPRO}/switch.cmake") +include("${DEVKITPRO}/cmake/Switch.cmake") set(NSWITCH TRUE) -# Enable gcc -g, to use -# /opt/devkitpro/devkitA64/bin/aarch64-none-elf-addr2line -e build_switch/switch/chiaki -f -p -C -a 0xCCB5C -# set(CMAKE_BUILD_TYPE Debug) -# set(CMAKE_POSITION_INDEPENDENT_CODE ON) -# set(BUILD_SHARED_LIBS OFF CACHE INTERNAL "Shared libs not available" ) - -# FIXME rework this file to use the toolchain only -# https://github.com/diasurgical/devilutionX/pull/764 -set(ARCH "-march=armv8-a+crc+crypto -mtune=cortex-a57 -mtp=soft -ftls-model=local-exec") -# set(CMAKE_C_FLAGS "-O2 -ffunction-sections ${ARCH}") -set(CMAKE_CXX_FLAGS "${CMAKE_C_FLAGS}") -# workaroud force -fPIE to avoid -# aarch64-none-elf/bin/ld: read-only segment has dynamic relocations -set(CMAKE_EXE_LINKER_FLAGS "-specs=${DEVKITPRO}/libnx/switch.specs ${ARCH} -fPIE -Wl,-Map,Output.map") - -# add portlibs to the list of include dir -include_directories("${PORTLIBS_PREFIX}/include") - # troubleshoot message(STATUS "CMAKE_FIND_ROOT_PATH = ${CMAKE_FIND_ROOT_PATH}") message(STATUS "PKG_CONFIG_EXECUTABLE = ${PKG_CONFIG_EXECUTABLE}") @@ -79,4 +60,3 @@ function(add_nro_target output_name target title author version icon romfs) endfunction() set(CMAKE_USE_SYSTEM_ENVIRONMENT_PATH OFF) -set(CMAKE_PREFIX_PATH "/") diff --git a/scripts/Dockerfile.bionic b/scripts/Dockerfile.bionic new file mode 100644 index 0000000..2548a2d --- /dev/null +++ b/scripts/Dockerfile.bionic @@ -0,0 +1,16 @@ + +FROM ubuntu:bionic + +RUN apt-get update +RUN apt-get install -y software-properties-common gpg wget +RUN add-apt-repository ppa:beineri/opt-qt-5.12.10-bionic +COPY kitware-archive-latest.asc /kitware-archive-latest.asc +RUN cat /kitware-archive-latest.asc | gpg --dearmor > /usr/share/keyrings/kitware-archive-keyring.gpg +RUN echo 'deb [signed-by=/usr/share/keyrings/kitware-archive-keyring.gpg] https://apt.kitware.com/ubuntu/ bionic main' > /etc/apt/sources.list.d/kitware.list +RUN apt-get update +RUN apt-get -y install git g++ cmake ninja-build curl pkg-config unzip python3-pip \ + libssl-dev libopus-dev qt512base qt512multimedia qt512svg \ + libgl1-mesa-dev nasm libudev-dev libva-dev fuse libevdev-dev libudev-dev + +CMD [] + diff --git a/scripts/Dockerfile.xenial b/scripts/Dockerfile.xenial deleted file mode 100644 index dd92581..0000000 --- a/scripts/Dockerfile.xenial +++ /dev/null @@ -1,13 +0,0 @@ - -FROM ubuntu:xenial - -RUN apt-get update -RUN apt-get install -y software-properties-common -RUN add-apt-repository ppa:beineri/opt-qt-5.12.3-xenial -RUN apt-get update -RUN apt-get -y install git g++ cmake ninja-build curl pkg-config unzip python3-pip \ - libssl-dev libopus-dev qt512base qt512multimedia qt512svg \ - libgl1-mesa-dev nasm libudev-dev libva-dev fuse libevdev-dev libudev-dev - -CMD [] - diff --git a/scripts/kitware-archive-latest.asc b/scripts/kitware-archive-latest.asc new file mode 100644 index 0000000..6b3a357 --- /dev/null +++ b/scripts/kitware-archive-latest.asc @@ -0,0 +1,64 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +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 +=cUKW +-----END PGP PUBLIC KEY BLOCK----- diff --git a/scripts/run-docker-build-appimage.sh b/scripts/run-docker-build-appimage.sh index 931499c..b8625c8 100755 --- a/scripts/run-docker-build-appimage.sh +++ b/scripts/run-docker-build-appimage.sh @@ -3,13 +3,13 @@ set -xe cd "`dirname $(readlink -f ${0})`" -docker build -t chiaki-xenial . -f Dockerfile.xenial +docker build -t chiaki-bionic . -f Dockerfile.bionic cd .. docker run --rm \ -v "`pwd`:/build/chiaki" \ -w "/build/chiaki" \ --device /dev/fuse \ --cap-add SYS_ADMIN \ - -t chiaki-xenial \ + -t chiaki-bionic \ /bin/bash -c "scripts/build-appimage.sh" diff --git a/scripts/switch/build.sh b/scripts/switch/build.sh index 1e03a2a..f04f4b2 100755 --- a/scripts/switch/build.sh +++ b/scripts/switch/build.sh @@ -5,10 +5,7 @@ set -xveo pipefail arg1=$1 build="./build" if [ "$arg1" != "linux" ]; then - # source /opt/devkitpro/switchvars.sh - # toolchain="${DEVKITPRO}/switch.cmake" toolchain="cmake/switch.cmake" - export PORTLIBS_PREFIX="$(${DEVKITPRO}/portlibs_prefix.sh switch)" build="./build_switch" fi diff --git a/scripts/switch/run-docker-build-chiaki.sh b/scripts/switch/run-docker-build-chiaki.sh index c81c788..c52933b 100755 --- a/scripts/switch/run-docker-build-chiaki.sh +++ b/scripts/switch/run-docker-build-chiaki.sh @@ -6,6 +6,6 @@ docker run \ -v "`pwd`:/build/chiaki" \ -w "/build/chiaki" \ -t \ - thestr4ng3r/chiaki-build-switch \ + thestr4ng3r/chiaki-build-switch:35829cc \ -c "scripts/switch/build.sh" diff --git a/switch/borealis b/switch/borealis index cbdc1b6..eae1371 160000 --- a/switch/borealis +++ b/switch/borealis @@ -1 +1 @@ -Subproject commit cbdc1b65314d1eeb2799deae5cf6f113d6d67b46 +Subproject commit eae1371831d6cebf11b8ebd4c611069bccc6fb9b From 420809b24ef7bb8510a13d9a4c02ac260a9841a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Thu, 2 Jun 2022 17:10:19 +0200 Subject: [PATCH 212/237] Add option to fetch mbedtls with cmake --- CMakeLists.txt | 12 ++++++++++++ lib/CMakeLists.txt | 14 +++++++++----- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 611af18..e9cc357 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -24,6 +24,7 @@ endif() tri_option(CHIAKI_ENABLE_FFMPEG_DECODER "Enable FFMPEG video decoder" ${CHIAKI_FFMPEG_DEFAULT}) tri_option(CHIAKI_ENABLE_PI_DECODER "Enable Raspberry Pi-specific video decoder (requires libraspberrypi0 and libraspberrypi-doc)" AUTO) option(CHIAKI_LIB_ENABLE_MBEDTLS "Use mbedtls instead of OpenSSL as part of Chiaki Lib" OFF) +option(CHIAKI_LIB_MBEDTLS_EXTERNAL_PROJECT "Fetch Mbed TLS instead of using system-provided libs" OFF) option(CHIAKI_LIB_OPENSSL_EXTERNAL_PROJECT "Use OpenSSL as CMake external project" OFF) option(CHIAKI_GUI_ENABLE_SDL_GAMECONTROLLER "Use SDL Gamecontroller for Input" ON) option(CHIAKI_CLI_ARGP_STANDALONE "Search for standalone argp lib for CLI" OFF) @@ -89,6 +90,17 @@ endif() if(CHIAKI_LIB_ENABLE_MBEDTLS) add_definitions(-DCHIAKI_LIB_ENABLE_MBEDTLS) + if(CHIAKI_LIB_MBEDTLS_EXTERNAL_PROJECT) + include(FetchContent) + set(ENABLE_TESTING OFF) + set(USE_SHARED_MBEDTLS_LIBRARY OFF) + FetchContent_Declare( + mbedtls + GIT_REPOSITORY https://github.com/Mbed-TLS/mbedtls.git + GIT_TAG 8b3f26a5ac38d4fdccbc5c5366229f3e01dafcc0 # v2.28.0 + ) + FetchContent_MakeAvailable(mbedtls) + endif() endif() if(CHIAKI_ENABLE_FFMPEG_DECODER) diff --git a/lib/CMakeLists.txt b/lib/CMakeLists.txt index 3eb6e27..cbfd6b0 100644 --- a/lib/CMakeLists.txt +++ b/lib/CMakeLists.txt @@ -119,11 +119,15 @@ find_package(Threads REQUIRED) target_link_libraries(chiaki-lib Threads::Threads) if(CHIAKI_LIB_ENABLE_MBEDTLS) - # provided by mbedtls-static (mbedtls-devel) - find_library(MBEDTLS mbedtls) - find_library(MBEDX509 mbedx509) - find_library(MBEDCRYPTO mbedcrypto) - target_link_libraries(chiaki-lib ${MBEDTLS} ${MBEDX509} ${MBEDCRYPTO}) + if(CHIAKI_LIB_MBEDTLS_EXTERNAL_PROJECT) + target_link_libraries(chiaki-lib mbedtls mbedx509 mbedcrypto) + else() + # provided by mbedtls-static (mbedtls-devel) + find_library(MBEDTLS mbedtls) + find_library(MBEDX509 mbedx509) + find_library(MBEDCRYPTO mbedcrypto) + target_link_libraries(chiaki-lib ${MBEDTLS} ${MBEDX509} ${MBEDCRYPTO}) + endif() elseif(CHIAKI_LIB_OPENSSL_EXTERNAL_PROJECT) target_link_libraries(chiaki-lib OpenSSL_Crypto) else() From b4f051395fdd781e826fc5eac75b6de45e1917b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Thu, 2 Jun 2022 17:50:18 +0200 Subject: [PATCH 213/237] Reduce targets in fetched mbedtls and show progress --- CMakeLists.txt | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index e9cc357..3a552a0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -91,13 +91,16 @@ endif() if(CHIAKI_LIB_ENABLE_MBEDTLS) add_definitions(-DCHIAKI_LIB_ENABLE_MBEDTLS) if(CHIAKI_LIB_MBEDTLS_EXTERNAL_PROJECT) + set(FETCHCONTENT_QUIET CACHE BOOL FALSE) include(FetchContent) - set(ENABLE_TESTING OFF) - set(USE_SHARED_MBEDTLS_LIBRARY OFF) + set(ENABLE_TESTING CACHE INTERNAL OFF) + set(ENABLE_PROGRAMS CACHE INTERNAL OFF) + set(USE_SHARED_MBEDTLS_LIBRARY CACHE INTERNAL OFF) FetchContent_Declare( mbedtls GIT_REPOSITORY https://github.com/Mbed-TLS/mbedtls.git GIT_TAG 8b3f26a5ac38d4fdccbc5c5366229f3e01dafcc0 # v2.28.0 + GIT_PROGRESS TRUE ) FetchContent_MakeAvailable(mbedtls) endif() From b790fb3fb5670fde67ab63571939ab03b529b7fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Thu, 2 Jun 2022 18:26:42 +0200 Subject: [PATCH 214/237] Set PATH to find protoc for nanopb generator --- lib/protobuf/CMakeLists.txt | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/lib/protobuf/CMakeLists.txt b/lib/protobuf/CMakeLists.txt index 6efa342..43fde68 100644 --- a/lib/protobuf/CMakeLists.txt +++ b/lib/protobuf/CMakeLists.txt @@ -11,8 +11,16 @@ add_custom_command(OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/takion.pb" set(SOURCE_FILES "${CMAKE_CURRENT_BINARY_DIR}/takion.pb.c") set(HEADER_FILES "${CMAKE_CURRENT_BINARY_DIR}/takion.pb.h") +if(UNIX AND IS_ABSOLUTE "${PROTOC}") + # make sure protoc is in PATH when invoking the generator below, which needs it. + get_filename_component(PROTOC_PATH "${PROTOC}" DIRECTORY) + set(GEN_PREFIX "${CMAKE_COMMAND}" -E env "PATH=${PROTOC_PATH}:$ENV{PATH}") +else() + set(GEN_PREFIX "") +endif() + add_custom_command(OUTPUT ${SOURCE_FILES} ${HEADER_FILES} - COMMAND "${PYTHON_EXECUTABLE}" "${NANOPB_GENERATOR_PY}" "${CMAKE_CURRENT_BINARY_DIR}/takion.pb" + COMMAND ${GEN_PREFIX} "${PYTHON_EXECUTABLE}" "${NANOPB_GENERATOR_PY}" "${CMAKE_CURRENT_BINARY_DIR}/takion.pb" MAIN_DEPENDENCY "${CMAKE_CURRENT_BINARY_DIR}/takion.pb") set(CHIAKI_LIB_PROTO_SOURCE_FILES "${SOURCE_FILES}" PARENT_SCOPE) From 4164255ef9ce8b74c908b4d24973368c78c997e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Sun, 11 Sep 2022 14:00:59 +0200 Subject: [PATCH 215/237] Update dependencies and fix CI * nanopb 0.4.6.4 with PB_C99_STATIC_ASSERT to avoid depending on C11 * OpenSSL 1.1.1q on windows * Switched from docker to podman in CI * Appdir in appimage build container is now in /build/appdir to fix "Invalid cross-device link" errors in linuxdeploy when appdir is in mount * Use python protobuf==3.19.5 in bionic image, which still supports python 3.6, and on windows where nanopb otherwise has issues * Purge nanopb_pb2.py to force regeneration of it for possibly different python protobuf versions in subsequent builds * FreeBSD needs py39 packages now --- .appveyor.yml | 1 + .builds/common.yml | 24 ++++++++++++------- .builds/freebsd.yml | 2 +- scripts/appveyor-win.sh | 4 ++-- scripts/build-appimage.sh | 13 +++++++--- scripts/build-common.sh | 3 +++ ...pimage.sh => run-podman-build-appimage.sh} | 6 ++--- ...llseye.sh => run-podman-build-bullseye.sh} | 5 ++-- scripts/switch/build.sh | 3 +++ ...-chiaki.sh => push-podman-build-chiaki.sh} | 0 ...d-chiaki.sh => run-podman-build-chiaki.sh} | 2 +- switch/README.md | 4 ++-- third-party/CMakeLists.txt | 2 ++ third-party/nanopb | 2 +- 14 files changed, 48 insertions(+), 23 deletions(-) rename scripts/{run-docker-build-appimage.sh => run-podman-build-appimage.sh} (58%) rename scripts/{run-docker-build-bullseye.sh => run-podman-build-bullseye.sh} (61%) rename scripts/switch/{push-docker-build-chiaki.sh => push-podman-build-chiaki.sh} (100%) rename scripts/switch/{run-docker-build-chiaki.sh => run-podman-build-chiaki.sh} (93%) diff --git a/.appveyor.yml b/.appveyor.yml index 2731ccb..7dc4799 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -5,6 +5,7 @@ image: branches: only: - master + - develop - /^v\d.*$/ - /^deploy-test(-.*)?$/ diff --git a/.builds/common.yml b/.builds/common.yml index 4681f14..0bce7cb 100644 --- a/.builds/common.yml +++ b/.builds/common.yml @@ -16,8 +16,9 @@ packages: - qt5-qtmultimedia-dev - ffmpeg-dev - sdl2-dev - - docker + - podman - fuse + - udev - argp-standalone artifacts: @@ -25,10 +26,17 @@ artifacts: - Chiaki.AppImage tasks: - - start_docker: | - sudo service docker start - sudo chmod +s /usr/bin/docker # Yes, I know what I am doing - sudo service fuse start # Fuse for AppImages + - setup_podman: | + sudo rc-service udev start + sudo rc-service cgroups start + sudo rc-service fuse start # Fuse for AppImages + echo build:100000:65536 | sudo tee /etc/subuid + echo build:100000:65536 | sudo tee /etc/subgid + # https://www.kernel.org/doc/Documentation/networking/tuntap.txt + # for slirp4netns + sudo mkdir -p /dev/net + sudo mknod /dev/net/tun c 10 200 + sudo chmod 0666 /dev/net/tun - local_build_and_test: | cd chiaki cmake -Bbuild -GNinja -DCHIAKI_ENABLE_CLI=ON -DCHIAKI_ENABLE_GUI=ON -DCHIAKI_CLI_ARGP_STANDALONE=ON @@ -36,12 +44,12 @@ tasks: build/test/chiaki-unit - appimage: | cd chiaki - scripts/run-docker-build-appimage.sh + scripts/run-podman-build-appimage.sh cp appimage/Chiaki.AppImage ../Chiaki.AppImage - switch: | cd chiaki - scripts/switch/run-docker-build-chiaki.sh + scripts/switch/run-podman-build-chiaki.sh cp build_switch/switch/chiaki.nro ../chiaki.nro - bullseye: | cd chiaki - scripts/run-docker-build-bullseye.sh + scripts/run-podman-build-bullseye.sh diff --git a/.builds/freebsd.yml b/.builds/freebsd.yml index a105aec..c62a695 100644 --- a/.builds/freebsd.yml +++ b/.builds/freebsd.yml @@ -7,7 +7,7 @@ sources: packages: - cmake - protobuf - - py38-protobuf + - py39-protobuf - opus - qt5-core - qt5-qmake diff --git a/scripts/appveyor-win.sh b/scripts/appveyor-win.sh index 1361114..3951d84 100755 --- a/scripts/appveyor-win.sh +++ b/scripts/appveyor-win.sh @@ -26,7 +26,7 @@ ninja || exit 1 ninja install || exit 1 cd ../.. || exit 1 -wget https://mirror.firedaemon.com/OpenSSL/openssl-1.1.1n.zip && 7z x openssl-1.1.1n.zip || exit 1 +wget https://mirror.firedaemon.com/OpenSSL/openssl-1.1.1q.zip && 7z x openssl-1.1.1q.zip || exit 1 wget https://www.libsdl.org/release/SDL2-devel-2.0.10-VC.zip && 7z x SDL2-devel-2.0.10-VC.zip || exit 1 export SDL_ROOT="$APPVEYOR_BUILD_FOLDER/SDL2-2.0.10" || exit 1 @@ -41,7 +41,7 @@ cd .. || exit 1 export PATH="$PWD/protoc/bin:$PATH" || exit 1 PYTHON="C:/Python37/python.exe" -"$PYTHON" -m pip install protobuf || exit 1 +"$PYTHON" -m pip install protobuf==3.19.5 || exit 1 QT_PATH="C:/Qt/5.15/msvc2019_64" diff --git a/scripts/build-appimage.sh b/scripts/build-appimage.sh index dd01ef4..83903fd 100755 --- a/scripts/build-appimage.sh +++ b/scripts/build-appimage.sh @@ -2,9 +2,12 @@ set -xe +# sometimes there are errors in linuxdeploy in docker/podman when the appdir is on a mount +appdir=${1:-`pwd`/appimage/appdir} + mkdir appimage -pip3 install --user protobuf +pip3 install --user protobuf==3.19.5 # need support for python 3.6 for running on bionic scripts/fetch-protoc.sh appimage export PATH="`pwd`/appimage/protoc/bin:$PATH" scripts/build-ffmpeg.sh appimage @@ -24,10 +27,14 @@ cmake \ -DCMAKE_INSTALL_PREFIX=/usr \ .. cd .. + +# purge leftover proto/nanopb_pb2.py which may have been created with another protobuf version +rm -fv third-party/nanopb/generator/proto/nanopb_pb2.py + ninja -C build_appimage build_appimage/test/chiaki-unit -DESTDIR=`pwd`/appimage/appdir ninja -C build_appimage install +DESTDIR="${appdir}" ninja -C build_appimage install cd appimage curl -L -O https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage @@ -41,5 +48,5 @@ set -e export LD_LIBRARY_PATH="`pwd`/sdl2-prefix/lib:$LD_LIBRARY_PATH" export EXTRA_QT_PLUGINS=opengl -./linuxdeploy-x86_64.AppImage --appdir=appdir -e appdir/usr/bin/chiaki -d appdir/usr/share/applications/chiaki.desktop --plugin qt --output appimage +./linuxdeploy-x86_64.AppImage --appdir="${appdir}" -e "${appdir}/usr/bin/chiaki" -d "${appdir}/usr/share/applications/chiaki.desktop" --plugin qt --output appimage mv Chiaki-*-x86_64.AppImage Chiaki.AppImage diff --git a/scripts/build-common.sh b/scripts/build-common.sh index 0eb87b9..dd4be2c 100755 --- a/scripts/build-common.sh +++ b/scripts/build-common.sh @@ -1,5 +1,8 @@ #!/bin/bash +# purge leftover proto/nanopb_pb2.py which may have been created with another protobuf version +rm -fv third-party/nanopb/generator/proto/nanopb_pb2.py + mkdir build && cd build || exit 1 cmake \ -DCMAKE_BUILD_TYPE=Release \ diff --git a/scripts/run-docker-build-appimage.sh b/scripts/run-podman-build-appimage.sh similarity index 58% rename from scripts/run-docker-build-appimage.sh rename to scripts/run-podman-build-appimage.sh index b8625c8..5dea4b4 100755 --- a/scripts/run-docker-build-appimage.sh +++ b/scripts/run-podman-build-appimage.sh @@ -3,13 +3,13 @@ set -xe cd "`dirname $(readlink -f ${0})`" -docker build -t chiaki-bionic . -f Dockerfile.bionic +podman build -t chiaki-bionic . -f Dockerfile.bionic cd .. -docker run --rm \ +podman run --rm \ -v "`pwd`:/build/chiaki" \ -w "/build/chiaki" \ --device /dev/fuse \ --cap-add SYS_ADMIN \ -t chiaki-bionic \ - /bin/bash -c "scripts/build-appimage.sh" + /bin/bash -c "scripts/build-appimage.sh /build/appdir" diff --git a/scripts/run-docker-build-bullseye.sh b/scripts/run-podman-build-bullseye.sh similarity index 61% rename from scripts/run-docker-build-bullseye.sh rename to scripts/run-podman-build-bullseye.sh index fd19bb1..ad5c6d1 100755 --- a/scripts/run-docker-build-bullseye.sh +++ b/scripts/run-podman-build-bullseye.sh @@ -3,10 +3,11 @@ set -xe cd "`dirname $(readlink -f ${0})`" -docker build -t chiaki-bullseye . -f Dockerfile.bullseye +podman build -t chiaki-bullseye . -f Dockerfile.bullseye cd .. -docker run --rm -v "`pwd`:/build" chiaki-bullseye /bin/bash -c " +podman run --rm -v "`pwd`:/build" chiaki-bullseye /bin/bash -c " cd /build && + rm -fv third-party/nanopb/generator/proto/nanopb_pb2.py && mkdir build_bullseye && cmake -Bbuild_bullseye -GNinja -DCHIAKI_ENABLE_SETSU=ON -DCHIAKI_USE_SYSTEM_JERASURE=ON -DCHIAKI_USE_SYSTEM_NANOPB=ON && ninja -C build_bullseye && diff --git a/scripts/switch/build.sh b/scripts/switch/build.sh index f04f4b2..37b825d 100755 --- a/scripts/switch/build.sh +++ b/scripts/switch/build.sh @@ -16,6 +16,9 @@ build_chiaki (){ pushd "${BASEDIR}" #rm -rf ./build + # purge leftover proto/nanopb_pb2.py which may have been created with another protobuf version + rm -fv third-party/nanopb/generator/proto/nanopb_pb2.py + cmake -B "${build}" \ -GNinja \ -DCMAKE_TOOLCHAIN_FILE=${toolchain} \ diff --git a/scripts/switch/push-docker-build-chiaki.sh b/scripts/switch/push-podman-build-chiaki.sh similarity index 100% rename from scripts/switch/push-docker-build-chiaki.sh rename to scripts/switch/push-podman-build-chiaki.sh diff --git a/scripts/switch/run-docker-build-chiaki.sh b/scripts/switch/run-podman-build-chiaki.sh similarity index 93% rename from scripts/switch/run-docker-build-chiaki.sh rename to scripts/switch/run-podman-build-chiaki.sh index c52933b..520188b 100755 --- a/scripts/switch/run-docker-build-chiaki.sh +++ b/scripts/switch/run-podman-build-chiaki.sh @@ -2,7 +2,7 @@ cd "`dirname $(readlink -f ${0})`/../.." -docker run \ +podman run \ -v "`pwd`:/build/chiaki" \ -w "/build/chiaki" \ -t \ diff --git a/switch/README.md b/switch/README.md index f5fbe68..2307fa4 100644 --- a/switch/README.md +++ b/switch/README.md @@ -7,7 +7,7 @@ but the easiest way is to use the following container. Build Project ------------- ``` -bash scripts/switch/run-docker-build-chiaki.sh +bash scripts/switch/run-podman-build-chiaki.sh ``` tools @@ -15,7 +15,7 @@ tools Push to homebrew Netloader ``` # where X.X.X.X is the IP of your switch -bash scripts/switch/push-docker-build-chiaki.sh -a 192.168.0.200 +bash scripts/switch/push-podman-build-chiaki.sh -a 192.168.0.200 ``` Troubleshoot diff --git a/third-party/CMakeLists.txt b/third-party/CMakeLists.txt index b1f0b46..f6ebc40 100644 --- a/third-party/CMakeLists.txt +++ b/third-party/CMakeLists.txt @@ -4,11 +4,13 @@ if(NOT CHIAKI_USE_SYSTEM_NANOPB) # nanopb ################## + add_definitions(-DPB_C99_STATIC_ASSERT) # Fix PB_STATIC_ASSERT on msvc without using C11 for now add_subdirectory(nanopb EXCLUDE_FROM_ALL) set(NANOPB_GENERATOR_PY "${CMAKE_CURRENT_SOURCE_DIR}/nanopb/generator/nanopb_generator.py" PARENT_SCOPE) add_library(nanopb INTERFACE) target_link_libraries(nanopb INTERFACE protobuf-nanopb-static) target_include_directories(nanopb INTERFACE "${CMAKE_CURRENT_SOURCE_DIR}/nanopb") + target_compile_definitions(nanopb INTERFACE -DPB_C99_STATIC_ASSERT) # see above add_library(Nanopb::nanopb ALIAS nanopb) endif() diff --git a/third-party/nanopb b/third-party/nanopb index ab19ecb..afc499f 160000 --- a/third-party/nanopb +++ b/third-party/nanopb @@ -1 +1 @@ -Subproject commit ab19ecbe1b9f377ab4ee8e762bfe16c39068ad68 +Subproject commit afc499f9a410fc9bbf6c9c48cdd8d8b199d49eb4 From e00f5fae9df25cb263c380202446caa4b40a8d30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Sat, 24 Sep 2022 19:47:31 +0200 Subject: [PATCH 216/237] Update macOS icon in Big Sur style --- assets/chiaki_macos.svg | 266 +++++++++++++++++++++++++++++++++ assets/chiaki_macos_simple.svg | 102 +++++++++++++ gui/chiaki.icns | Bin 164978 -> 363706 bytes gui/res/chiaki_macos.svg | 102 +++++++++++++ gui/res/resources.qrc | 1 + gui/src/main.cpp | 4 + 6 files changed, 475 insertions(+) create mode 100644 assets/chiaki_macos.svg create mode 100644 assets/chiaki_macos_simple.svg create mode 100644 gui/res/chiaki_macos.svg diff --git a/assets/chiaki_macos.svg b/assets/chiaki_macos.svg new file mode 100644 index 0000000..a4ce8f4 --- /dev/null +++ b/assets/chiaki_macos.svg @@ -0,0 +1,266 @@ + + + + diff --git a/assets/chiaki_macos_simple.svg b/assets/chiaki_macos_simple.svg new file mode 100644 index 0000000..7f1359d --- /dev/null +++ b/assets/chiaki_macos_simple.svg @@ -0,0 +1,102 @@ + + + + diff --git a/gui/chiaki.icns b/gui/chiaki.icns index 13613ffeacadacdc69503fa828f7386ed8b71e4e..e51fa65a4688c29e0553810e9a27b44fa3316c5d 100644 GIT binary patch literal 363706 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
    lkzjiz%8l=zbwu(2oYHODCe^+(G#lFp;8 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ⅅ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㱉#sl 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 RDgjd<_&D_I%U}P_{U4kf>vaGC literal 164978 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
    `!zioD`*0#QZ)~UtbrKwHc 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 + + + diff --git a/gui/res/resources.qrc b/gui/res/resources.qrc index 86efda9..b89bee7 100644 --- a/gui/res/resources.qrc +++ b/gui/res/resources.qrc @@ -5,6 +5,7 @@ discover-24px.svg discover-off-24px.svg chiaki.svg + chiaki_macos.svg console-ps4.svg console-ps5.svg diff --git a/gui/src/main.cpp b/gui/src/main.cpp index a96f007..e6597cf 100644 --- a/gui/src/main.cpp +++ b/gui/src/main.cpp @@ -71,7 +71,11 @@ int real_main(int argc, char *argv[]) QApplication app(argc, argv); +#ifdef Q_OS_MACOS + QApplication::setWindowIcon(QIcon(":/icons/chiaki_macos.svg")); +#else QApplication::setWindowIcon(QIcon(":/icons/chiaki.svg")); +#endif QApplication::setAttribute(Qt::AA_UseHighDpiPixmaps); From 6bfbcfc456207803a0f648eef1e6d291c868aa31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Sun, 25 Sep 2022 14:24:43 +0200 Subject: [PATCH 217/237] Update chiaki-build-switch image in build script --- scripts/switch/run-podman-build-chiaki.sh | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/scripts/switch/run-podman-build-chiaki.sh b/scripts/switch/run-podman-build-chiaki.sh index 520188b..f9e05c0 100755 --- a/scripts/switch/run-podman-build-chiaki.sh +++ b/scripts/switch/run-podman-build-chiaki.sh @@ -2,10 +2,10 @@ cd "`dirname $(readlink -f ${0})`/../.." -podman run \ +podman run --rm \ -v "`pwd`:/build/chiaki" \ -w "/build/chiaki" \ - -t \ - thestr4ng3r/chiaki-build-switch:35829cc \ - -c "scripts/switch/build.sh" + -it \ + thestr4ng3r/chiaki-build-switch:v2 \ + /bin/bash -c "scripts/switch/build.sh" From 40a9dee4edc27bbfc03813ed3d410bbed44edc3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Sun, 23 Oct 2022 13:42:12 +0200 Subject: [PATCH 218/237] Fix some EINTR handling --- README.md | 2 +- lib/src/http.c | 10 +++++++++- lib/src/stoppipe.c | 6 +++++- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index f306470..5be248c 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ [![AppVeyor Build status](https://ci.appveyor.com/api/projects/status/c81ogebvsmo43dd3?svg=true)](https://ci.appveyor.com/project/thestr4ng3r/chiaki) [![builds.sr.ht Status](https://builds.sr.ht/~thestr4ng3r/chiaki.svg)](https://builds.sr.ht/~thestr4ng3r/chiaki?) Chiaki is a Free and Open Source Software Client for PlayStation 4 and PlayStation 5 Remote Play -for Linux, FreeBSD, OpenBSD, Android, macOS, Windows, Nintendo Switch and potentially even more platforms. +for Linux, FreeBSD, OpenBSD, NetBSD, Android, macOS, Windows, Nintendo Switch and potentially even more platforms. ![Screenshot](assets/screenshot.png) diff --git a/lib/src/http.c b/lib/src/http.c index f55c438..5c07802 100644 --- a/lib/src/http.c +++ b/lib/src/http.c @@ -146,7 +146,15 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_recv_http_header(int sock, char *buf, size_ return err; } - int received = (int)recv(sock, buf, (int)buf_size, 0); + int received; + do + { + received = (int)recv(sock, buf, (int)buf_size, 0); +#if _WIN32 + } while(false); +#else + } while(received < 0 && errno == EINTR); +#endif if(received <= 0) return received == 0 ? CHIAKI_ERR_DISCONNECTED : CHIAKI_ERR_NETWORK; diff --git a/lib/src/stoppipe.c b/lib/src/stoppipe.c index 7ad2d2a..003ad5d 100644 --- a/lib/src/stoppipe.c +++ b/lib/src/stoppipe.c @@ -147,7 +147,11 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_stop_pipe_select_single(ChiakiStopPipe *sto timeout = &timeout_s; } - int r = select(nfds, &rfds, write ? &wfds : NULL, NULL, timeout); + int r; + do + { + r = select(nfds, &rfds, write ? &wfds : NULL, NULL, timeout); + } while(r < 0 && errno == EINTR); if(r < 0) return CHIAKI_ERR_UNKNOWN; From 74d39e6314b8ff8ad7dbfd55fc44cae5a383aa25 Mon Sep 17 00:00:00 2001 From: Johannes Baiter Date: Tue, 1 Nov 2022 10:01:21 +0100 Subject: [PATCH 219/237] lib: Add support for trigger effects and controller haptics By default, no trigger effects and haptics are requested from the console, lib users have to explicitly enable them for a session by setting the new `enable_dualsense` flag on the session's `ChiakiConnectInfo` struct. Trigger Effects are simply a new Takion message type `11` and include the type of each effect and the effect data (10 bytes) for each of the triggers. They are exposed as a new Chiaki event type `CHIAKI_EVENT_TRIGGER_EFFECTS`. Haptic effects are implemented in the protocol as a separate audio stream, for which packets are only sent when there are actually effects being played, i.e. silence is not explicitly encoded. Audio data is 3kHz little endian 16 bit stereo sent in frames of 10 samples every 100ms. Note that the Takion AV header has the codec field set to Opus, however this is not true. Users can provide a new `ChiakiAudioSink` dedicated to haptics via the new `chiaki_session_set_haptics_sink` API, which behaves identical to the regular audio sink, except that it has a lower frequency. --- lib/include/chiaki/feedback.h | 2 +- lib/include/chiaki/session.h | 21 +++++++ lib/include/chiaki/streamconnection.h | 1 + lib/include/chiaki/takion.h | 9 ++- lib/protobuf/takion.proto | 3 +- lib/src/audioreceiver.c | 10 ++-- lib/src/ctrl.c | 30 ++++++---- lib/src/feedback.c | 4 +- lib/src/session.c | 1 + lib/src/streamconnection.c | 83 ++++++++++++++++++++++++++- lib/src/takion.c | 9 ++- 11 files changed, 145 insertions(+), 28 deletions(-) diff --git a/lib/include/chiaki/feedback.h b/lib/include/chiaki/feedback.h index be66384..d0be7a1 100644 --- a/lib/include/chiaki/feedback.h +++ b/lib/include/chiaki/feedback.h @@ -38,7 +38,7 @@ CHIAKI_EXPORT void chiaki_feedback_state_format_v9(uint8_t *buf, ChiakiFeedbackS /** * @param buf buffer of at least CHIAKI_FEEDBACK_STATE_BUF_SIZE_V12 */ -CHIAKI_EXPORT void chiaki_feedback_state_format_v12(uint8_t *buf, ChiakiFeedbackState *state); +CHIAKI_EXPORT void chiaki_feedback_state_format_v12(uint8_t *buf, ChiakiFeedbackState *state, bool enable_dualsense); #define CHIAKI_HISTORY_EVENT_SIZE_MAX 0x5 diff --git a/lib/include/chiaki/session.h b/lib/include/chiaki/session.h index 3a60417..4f9f932 100644 --- a/lib/include/chiaki/session.h +++ b/lib/include/chiaki/session.h @@ -78,6 +78,7 @@ typedef struct chiaki_connect_info_t ChiakiConnectVideoProfile video_profile; bool video_profile_auto_downgrade; // Downgrade video_profile if server does not seem to support it. bool enable_keyboard; + bool enable_dualsense; } ChiakiConnectInfo; @@ -121,6 +122,14 @@ typedef struct chiaki_rumble_event_t uint8_t right; // high-frequency } ChiakiRumbleEvent; +typedef struct chiaki_trigger_effects_event_t +{ + uint8_t type_left; + uint8_t type_right; + uint8_t left[10]; + uint8_t right[10]; +} ChiakiTriggerEffectsEvent; + typedef enum { CHIAKI_EVENT_CONNECTED, CHIAKI_EVENT_LOGIN_PIN_REQUEST, @@ -129,6 +138,7 @@ typedef enum { CHIAKI_EVENT_KEYBOARD_REMOTE_CLOSE, CHIAKI_EVENT_RUMBLE, CHIAKI_EVENT_QUIT, + CHIAKI_EVENT_TRIGGER_EFFECTS, } ChiakiEventType; typedef struct chiaki_event_t @@ -139,6 +149,7 @@ typedef struct chiaki_event_t ChiakiQuitEvent quit; ChiakiKeyboardEvent keyboard; ChiakiRumbleEvent rumble; + ChiakiTriggerEffectsEvent trigger_effects; struct { bool pin_incorrect; // false on first request, true if the pin entered before was incorrect @@ -170,6 +181,7 @@ typedef struct chiaki_session_t ChiakiConnectVideoProfile video_profile; bool video_profile_auto_downgrade; bool enable_keyboard; + bool enable_dualsense; } connect_info; ChiakiTarget target; @@ -191,6 +203,7 @@ typedef struct chiaki_session_t ChiakiVideoSampleCallback video_sample_cb; void *video_sample_cb_user; ChiakiAudioSink audio_sink; + ChiakiAudioSink haptics_sink; ChiakiThread session_thread; @@ -246,6 +259,14 @@ static inline void chiaki_session_set_audio_sink(ChiakiSession *session, ChiakiA session->audio_sink = *sink; } +/** + * @param sink contents are copied + */ +static inline void chiaki_session_set_haptics_sink(ChiakiSession *session, ChiakiAudioSink *sink) +{ + session->haptics_sink = *sink; +} + #ifdef __cplusplus } #endif diff --git a/lib/include/chiaki/streamconnection.h b/lib/include/chiaki/streamconnection.h index ba1817a..90578c6 100644 --- a/lib/include/chiaki/streamconnection.h +++ b/lib/include/chiaki/streamconnection.h @@ -32,6 +32,7 @@ typedef struct chiaki_stream_connection_t ChiakiPacketStats packet_stats; ChiakiAudioReceiver *audio_receiver; ChiakiVideoReceiver *video_receiver; + ChiakiAudioReceiver *haptics_receiver; ChiakiFeedbackSender feedback_sender; /** diff --git a/lib/include/chiaki/takion.h b/lib/include/chiaki/takion.h index 1715842..15d62e5 100644 --- a/lib/include/chiaki/takion.h +++ b/lib/include/chiaki/takion.h @@ -27,7 +27,8 @@ extern "C" { typedef enum chiaki_takion_message_data_type_t { CHIAKI_TAKION_MESSAGE_DATA_TYPE_PROTOBUF = 0, CHIAKI_TAKION_MESSAGE_DATA_TYPE_RUMBLE = 7, - CHIAKI_TAKION_MESSAGE_DATA_TYPE_9 = 9 + CHIAKI_TAKION_MESSAGE_DATA_TYPE_9 = 9, + CHIAKI_TAKION_MESSAGE_DATA_TYPE_TRIGGER_EFFECTS = 11, } ChiakiTakionMessageDataType; typedef struct chiaki_takion_av_packet_t @@ -36,6 +37,7 @@ typedef struct chiaki_takion_av_packet_t ChiakiSeqNum16 frame_index; bool uses_nalu_info_structs; bool is_video; + bool is_haptics; ChiakiSeqNum16 unit_index; uint16_t units_in_frame_total; // source + units_in_frame_fec uint16_t units_in_frame_fec; @@ -46,8 +48,6 @@ typedef struct chiaki_takion_av_packet_t uint64_t key_pos; - uint8_t byte_before_audio_data; - uint8_t *data; // not owned size_t data_size; } ChiakiTakionAVPacket; @@ -106,6 +106,7 @@ typedef struct chiaki_takion_connect_info_t ChiakiTakionCallback cb; void *cb_user; bool enable_crypt; + bool enable_dualsense; uint8_t protocol_version; } ChiakiTakionConnectInfo; @@ -162,6 +163,8 @@ typedef struct chiaki_takion_t ChiakiTakionAVPacketParse av_packet_parse; ChiakiKeyState key_state; + + bool enable_dualsense; } ChiakiTakion; diff --git a/lib/protobuf/takion.proto b/lib/protobuf/takion.proto index 748d70a..ceef0f1 100644 --- a/lib/protobuf/takion.proto +++ b/lib/protobuf/takion.proto @@ -312,7 +312,8 @@ message ControllerConnectionPayload { VITA = 3; XINPUT = 4; MOBILE = 5; - BOND = 6; + DUALSENSE = 6; + VR2SENSE = 7; } } diff --git a/lib/src/audioreceiver.c b/lib/src/audioreceiver.c index 9fec69d..744d807 100644 --- a/lib/src/audioreceiver.c +++ b/lib/src/audioreceiver.c @@ -5,7 +5,7 @@ #include -static void chiaki_audio_receiver_frame(ChiakiAudioReceiver *audio_receiver, ChiakiSeqNum16 frame_index, uint8_t *buf, size_t buf_size); +static void chiaki_audio_receiver_frame(ChiakiAudioReceiver *audio_receiver, ChiakiSeqNum16 frame_index, bool is_haptics, uint8_t *buf, size_t buf_size); CHIAKI_EXPORT ChiakiErrorCode chiaki_audio_receiver_init(ChiakiAudioReceiver *audio_receiver, ChiakiSession *session, ChiakiPacketStats *packet_stats) { @@ -102,14 +102,14 @@ CHIAKI_EXPORT void chiaki_audio_receiver_av_packet(ChiakiAudioReceiver *audio_re frame_index = packet->frame_index - fec_units_count + fec_index; } - chiaki_audio_receiver_frame(audio_receiver, frame_index, packet->data + unit_size * i, unit_size); + chiaki_audio_receiver_frame(audio_receiver, frame_index, packet->is_haptics, packet->data + unit_size * i, unit_size); } if(audio_receiver->packet_stats) chiaki_packet_stats_push_seq(audio_receiver->packet_stats, packet->frame_index); } -static void chiaki_audio_receiver_frame(ChiakiAudioReceiver *audio_receiver, ChiakiSeqNum16 frame_index, uint8_t *buf, size_t buf_size) +static void chiaki_audio_receiver_frame(ChiakiAudioReceiver *audio_receiver, ChiakiSeqNum16 frame_index, bool is_haptics, uint8_t *buf, size_t buf_size) { chiaki_mutex_lock(&audio_receiver->mutex); @@ -117,7 +117,9 @@ static void chiaki_audio_receiver_frame(ChiakiAudioReceiver *audio_receiver, Chi goto beach; audio_receiver->frame_index_prev = frame_index; - if(audio_receiver->session->audio_sink.frame_cb) + if(is_haptics && audio_receiver->session->haptics_sink.frame_cb) + audio_receiver->session->haptics_sink.frame_cb(buf, buf_size, audio_receiver->session->haptics_sink.user); + else if(!is_haptics && audio_receiver->session->audio_sink.frame_cb) audio_receiver->session->audio_sink.frame_cb(buf, buf_size, audio_receiver->session->audio_sink.user); beach: diff --git a/lib/src/ctrl.c b/lib/src/ctrl.c index 9dfc277..29a39b2 100644 --- a/lib/src/ctrl.c +++ b/lib/src/ctrl.c @@ -488,17 +488,25 @@ static void ctrl_message_received(ChiakiCtrl *ctrl, uint16_t msg_type, uint8_t * static void ctrl_enable_optional_features(ChiakiCtrl *ctrl) { - if(!ctrl->session->connect_info.enable_keyboard) - return; - // TODO: Last byte of pre_enable request is random (?) - // TODO: Signature ?! - uint8_t enable = 1; - uint8_t pre_enable[4] = { 0x00, 0x01, 0x01, 0x80 }; - uint8_t signature[0x10] = { 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x05, 0xAE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; - ctrl_message_send(ctrl, 0xD, signature, 0x10); - ctrl_message_send(ctrl, 0x36, pre_enable, 4); - ctrl_message_send(ctrl, CTRL_MESSAGE_TYPE_KEYBOARD_ENABLE_TOGGLE, &enable, 1); - ctrl_message_send(ctrl, 0x36, pre_enable, 4); + if(ctrl->session->connect_info.enable_dualsense) + { + CHIAKI_LOGI(ctrl->session->log, "Enabling DualSense features"); + const uint8_t enable[3] = { 0x00, 0x40, 0x00 }; + ctrl_message_send(ctrl, 0x13, enable, 3); + } + if(ctrl->session->connect_info.enable_keyboard) + { + CHIAKI_LOGI(ctrl->session->log, "Enabling Keyboard"); + // TODO: Last byte of pre_enable request is random (?) + // TODO: Signature ?! + uint8_t enable = 1; + uint8_t pre_enable[4] = { 0x00, 0x01, 0x01, 0x80 }; + uint8_t signature[0x10] = { 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x05, 0xAE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; + ctrl_message_send(ctrl, 0xD, signature, 0x10); + ctrl_message_send(ctrl, 0x36, pre_enable, 4); + ctrl_message_send(ctrl, CTRL_MESSAGE_TYPE_KEYBOARD_ENABLE_TOGGLE, &enable, 1); + ctrl_message_send(ctrl, 0x36, pre_enable, 4); + } } static void ctrl_message_received_session_id(ChiakiCtrl *ctrl, uint8_t *payload, size_t payload_size) diff --git a/lib/src/feedback.c b/lib/src/feedback.c index 1ae190e..d585a88 100644 --- a/lib/src/feedback.c +++ b/lib/src/feedback.c @@ -76,12 +76,12 @@ CHIAKI_EXPORT void chiaki_feedback_state_format_v9(uint8_t *buf, ChiakiFeedbackS *((chiaki_unaligned_uint16_t *)(buf + 0x17)) = htons((uint16_t)state->right_y); } -CHIAKI_EXPORT void chiaki_feedback_state_format_v12(uint8_t *buf, ChiakiFeedbackState *state) +CHIAKI_EXPORT void chiaki_feedback_state_format_v12(uint8_t *buf, ChiakiFeedbackState *state, bool enable_dualsense) { chiaki_feedback_state_format_v9(buf, state); buf[0x19] = 0x0; buf[0x1a] = 0x0; - buf[0x1b] = 0x1; // 1 for Shock, 0 for Sense + buf[0x1b] = enable_dualsense ? 0x0 : 0x1; } CHIAKI_EXPORT ChiakiErrorCode chiaki_feedback_history_event_set_button(ChiakiFeedbackHistoryEvent *event, uint64_t button, uint8_t state) diff --git a/lib/src/session.c b/lib/src/session.c index b5b928f..ea8e09d 100644 --- a/lib/src/session.c +++ b/lib/src/session.c @@ -227,6 +227,7 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_session_init(ChiakiSession *session, Chiaki session->connect_info.video_profile = connect_info->video_profile; session->connect_info.video_profile_auto_downgrade = connect_info->video_profile_auto_downgrade; session->connect_info.enable_keyboard = connect_info->enable_keyboard; + session->connect_info.enable_dualsense = connect_info->enable_dualsense; return CHIAKI_ERR_SUCCESS; error_stop_pipe: diff --git a/lib/src/streamconnection.c b/lib/src/streamconnection.c index 2a7bb6f..7187c1b 100644 --- a/lib/src/streamconnection.c +++ b/lib/src/streamconnection.c @@ -47,7 +47,9 @@ static void stream_connection_takion_cb(ChiakiTakionEvent *event, void *user); static void stream_connection_takion_data(ChiakiStreamConnection *stream_connection, ChiakiTakionMessageDataType data_type, uint8_t *buf, size_t buf_size); static void stream_connection_takion_data_protobuf(ChiakiStreamConnection *stream_connection, uint8_t *buf, size_t buf_size); static void stream_connection_takion_data_rumble(ChiakiStreamConnection *stream_connection, uint8_t *buf, size_t buf_size); +static void stream_connection_takion_data_trigger_effects(ChiakiStreamConnection *stream_connection, uint8_t *buf, size_t buf_size); static ChiakiErrorCode stream_connection_send_big(ChiakiStreamConnection *stream_connection); +static ChiakiErrorCode stream_connection_send_controller_connection(ChiakiStreamConnection *stream_connection); static ChiakiErrorCode stream_connection_send_disconnect(ChiakiStreamConnection *stream_connection); static void stream_connection_takion_data_idle(ChiakiStreamConnection *stream_connection, uint8_t *buf, size_t buf_size); static void stream_connection_takion_data_expect_bang(ChiakiStreamConnection *stream_connection, uint8_t *buf, size_t buf_size); @@ -79,6 +81,7 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_stream_connection_init(ChiakiStreamConnecti stream_connection->video_receiver = NULL; stream_connection->audio_receiver = NULL; + stream_connection->haptics_receiver = NULL; err = chiaki_mutex_init(&stream_connection->feedback_sender_mutex, false); if(err != CHIAKI_ERR_SUCCESS) @@ -143,6 +146,7 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_stream_connection_run(ChiakiStreamConnectio takion_info.ip_dontfrag = false; takion_info.enable_crypt = true; + takion_info.enable_dualsense = session->connect_info.enable_dualsense; takion_info.protocol_version = chiaki_target_is_ps5(session->target) ? 12 : 9; takion_info.cb = stream_connection_takion_cb; @@ -164,12 +168,20 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_stream_connection_run(ChiakiStreamConnectio return CHIAKI_ERR_UNKNOWN; } + stream_connection->haptics_receiver = chiaki_audio_receiver_new(session, NULL); + if(!stream_connection->haptics_receiver) + { + CHIAKI_LOGE(session->log, "StreamConnection failed to initialize Haptics Receiver"); + err = CHIAKI_ERR_UNKNOWN; + goto err_audio_receiver; + } + stream_connection->video_receiver = chiaki_video_receiver_new(session, &stream_connection->packet_stats); if(!stream_connection->video_receiver) { CHIAKI_LOGE(session->log, "StreamConnection failed to initialize Video Receiver"); err = CHIAKI_ERR_UNKNOWN; - goto err_audio_receiver; + goto err_haptics_receiver; } stream_connection->state = STATE_TAKION_CONNECT; @@ -321,6 +333,10 @@ err_video_receiver: chiaki_video_receiver_free(stream_connection->video_receiver); stream_connection->video_receiver = NULL; +err_haptics_receiver: + chiaki_audio_receiver_free(stream_connection->haptics_receiver); + stream_connection->haptics_receiver = NULL; + err_audio_receiver: chiaki_audio_receiver_free(stream_connection->audio_receiver); stream_connection->audio_receiver = NULL; @@ -376,6 +392,9 @@ static void stream_connection_takion_data(ChiakiStreamConnection *stream_connect case CHIAKI_TAKION_MESSAGE_DATA_TYPE_RUMBLE: stream_connection_takion_data_rumble(stream_connection, buf, buf_size); break; + case CHIAKI_TAKION_MESSAGE_DATA_TYPE_TRIGGER_EFFECTS: + stream_connection_takion_data_trigger_effects(stream_connection, buf, buf_size); + break; default: break; } @@ -415,6 +434,24 @@ static void stream_connection_takion_data_rumble(ChiakiStreamConnection *stream_ chiaki_session_send_event(stream_connection->session, &event); } + +static void stream_connection_takion_data_trigger_effects(ChiakiStreamConnection *stream_connection, uint8_t *buf, size_t buf_size) +{ + if(buf_size < 25) + { + CHIAKI_LOGE(stream_connection->log, "StreamConnection got trigger effects packet with size %#llx < 25", + (unsigned long long)buf_size); + return; + } + ChiakiEvent event = { 0 }; + event.type = CHIAKI_EVENT_TRIGGER_EFFECTS; + event.trigger_effects.type_left = buf[1]; + event.trigger_effects.type_right = buf[2]; + memcpy(&event.trigger_effects.left, buf + 5, 10); + memcpy(&event.trigger_effects.right, buf + 15, 10); + chiaki_session_send_event(stream_connection->session, &event); +} + static void stream_connection_takion_data_handle_disconnect(ChiakiStreamConnection *stream_connection, uint8_t *buf, size_t buf_size) { tkproto_TakionMessage msg; @@ -460,7 +497,7 @@ static void stream_connection_takion_data_idle(ChiakiStreamConnection *stream_co return; } - CHIAKI_LOGV(stream_connection->log, "StreamConnection received data"); + CHIAKI_LOGV(stream_connection->log, "StreamConnection received data with msg.type == %d", msg.type); chiaki_log_hexdump(stream_connection->log, CHIAKI_LOG_VERBOSE, buf, buf_size); if(msg.type == tkproto_TakionMessage_PayloadType_DISCONNECT) @@ -522,7 +559,8 @@ static void stream_connection_takion_data_expect_bang(ChiakiStreamConnection *st return; } - CHIAKI_LOGE(stream_connection->log, "StreamConnection expected bang payload but received something else"); + CHIAKI_LOGE(stream_connection->log, "StreamConnection expected bang payload but received something else: %d", msg.type); + chiaki_log_hexdump(stream_connection->log, CHIAKI_LOG_VERBOSE, buf, buf_size); return; } @@ -584,6 +622,12 @@ static void stream_connection_takion_data_expect_bang(ChiakiStreamConnection *st // stream_connection->state_mutex is expected to be locked by the caller of this function stream_connection->state_finished = true; chiaki_cond_signal(&stream_connection->state_cond); + err = stream_connection_send_controller_connection(stream_connection); + if(err != CHIAKI_ERR_SUCCESS) + { + CHIAKI_LOGE(stream_connection->log, "StreamConnection failed to send controller connection"); + goto error; + } return; error: stream_connection->state_failed = true; @@ -811,6 +855,37 @@ static ChiakiErrorCode stream_connection_send_big(ChiakiStreamConnection *stream return err; } +static ChiakiErrorCode stream_connection_send_controller_connection(ChiakiStreamConnection *stream_connection) +{ + ChiakiSession *session = stream_connection->session; + tkproto_TakionMessage msg; + memset(&msg, 0, sizeof(msg)); + + msg.type = tkproto_TakionMessage_PayloadType_CONTROLLERCONNECTION; + msg.has_controller_connection_payload = true; + msg.controller_connection_payload.has_connected = true; + msg.controller_connection_payload.connected = true; + msg.controller_connection_payload.has_controller_id = false; + msg.controller_connection_payload.has_controller_type = true; + msg.controller_connection_payload.controller_type = session->connect_info.enable_dualsense + ? tkproto_ControllerConnectionPayload_ControllerType_DUALSENSE + : tkproto_ControllerConnectionPayload_ControllerType_DUALSHOCK4; + + uint8_t buf[2048]; + size_t buf_size; + + pb_ostream_t stream = pb_ostream_from_buffer(buf, sizeof(buf)); + bool pbr = pb_encode(&stream, tkproto_TakionMessage_fields, &msg); + if(!pbr) + { + CHIAKI_LOGE(stream_connection->log, "StreamConnection controller connection protobuf encoding failed"); + return CHIAKI_ERR_UNKNOWN; + } + + buf_size = stream.bytes_written; + return chiaki_takion_send_message_data(&stream_connection->takion, 1, 1, buf, buf_size, NULL); +} + static ChiakiErrorCode stream_connection_send_streaminfo_ack(ChiakiStreamConnection *stream_connection) { tkproto_TakionMessage msg; @@ -867,6 +942,8 @@ static void stream_connection_takion_av(ChiakiStreamConnection *stream_connectio if(packet->is_video) chiaki_video_receiver_av_packet(stream_connection->video_receiver, packet); + else if(packet->is_haptics) + chiaki_audio_receiver_av_packet(stream_connection->haptics_receiver, packet); else chiaki_audio_receiver_av_packet(stream_connection->audio_receiver, packet); } diff --git a/lib/src/takion.c b/lib/src/takion.c index c433977..f786d6f 100644 --- a/lib/src/takion.c +++ b/lib/src/takion.c @@ -57,7 +57,8 @@ typedef enum takion_packet_type_t { TAKION_PACKET_TYPE_CONGESTION = 5, TAKION_PACKET_TYPE_FEEDBACK_STATE = 6, TAKION_PACKET_TYPE_CLIENT_INFO = 8, - TAKION_PACKET_TYPE_PAD_INFO_EVENT = 9 + TAKION_PACKET_TYPE_PAD_INFO_EVENT = 9, + TAKION_PACKET_TYPE_PAD_ADAPTIVE_TRIGGERS = 11, } TakionPacketType; /** @@ -215,6 +216,7 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_takion_connect(ChiakiTakion *takion, Chiaki takion->postponed_packets = NULL; takion->postponed_packets_size = 0; takion->postponed_packets_count = 0; + takion->enable_dualsense = info->enable_dualsense; CHIAKI_LOGI(takion->log, "Takion connecting (version %u)", (unsigned int)info->protocol_version); @@ -556,7 +558,7 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_takion_send_feedback_state(ChiakiTakion *ta else { buf_sz = 0xc + CHIAKI_FEEDBACK_STATE_BUF_SIZE_V12; - chiaki_feedback_state_format_v12(buf + 0xc, feedback_state); + chiaki_feedback_state_format_v12(buf + 0xc, feedback_state, takion->enable_dualsense); } return takion_send_feedback_packet(takion, buf, buf_sz); } @@ -950,6 +952,7 @@ static void takion_flush_data_queue(ChiakiTakion *takion) if(data_type != CHIAKI_TAKION_MESSAGE_DATA_TYPE_PROTOBUF && data_type != CHIAKI_TAKION_MESSAGE_DATA_TYPE_RUMBLE + && data_type != CHIAKI_TAKION_MESSAGE_DATA_TYPE_TRIGGER_EFFECTS && data_type != CHIAKI_TAKION_MESSAGE_DATA_TYPE_9) { CHIAKI_LOGW(takion->log, "Takion received data with unexpected data type %#x", data_type); @@ -1308,7 +1311,7 @@ static ChiakiErrorCode av_packet_parse(bool v12, ChiakiTakionAVPacket *packet, C if(v12 && !packet->is_video) { - packet->byte_before_audio_data = *av; + packet->is_haptics = *av == 0x02; av += 1; av_size -= 1; } From 801f902bea43353ea00bd70de959f499021b87d8 Mon Sep 17 00:00:00 2001 From: Street Pea Date: Sat, 10 Dec 2022 15:09:43 +0100 Subject: [PATCH 220/237] Add transform/scaling modes to GUI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Added zoom and stretch modes to GUI to mirror the transform modes available on Android. They are reachable through a context menu or shortcuts (Ctrl+S/Ctrl+Z). CLI options --stretch and --zoom have been added as well. Co-authored-by: Florian Märkl --- gui/include/avopenglwidget.h | 11 +++- gui/include/streamsession.h | 13 ++++- gui/include/streamwindow.h | 6 ++ gui/include/transformmode.h | 12 ++++ gui/src/avopenglwidget.cpp | 36 ++++++++++-- gui/src/main.cpp | 24 +++++++- gui/src/mainwindow.cpp | 9 ++- gui/src/streamsession.cpp | 15 ++++- gui/src/streamwindow.cpp | 109 ++++++++++++++++++++++++++++++----- 9 files changed, 207 insertions(+), 28 deletions(-) create mode 100644 gui/include/transformmode.h diff --git a/gui/include/avopenglwidget.h b/gui/include/avopenglwidget.h index 6f234e9..19bd287 100644 --- a/gui/include/avopenglwidget.h +++ b/gui/include/avopenglwidget.h @@ -3,6 +3,8 @@ #ifndef CHIAKI_AVOPENGLWIDGET_H #define CHIAKI_AVOPENGLWIDGET_H +#include "transformmode.h" + #include #include @@ -74,21 +76,24 @@ class AVOpenGLWidget: public QOpenGLWidget public: static QSurfaceFormat CreateSurfaceFormat(); - explicit AVOpenGLWidget(StreamSession *session, QWidget *parent = nullptr); + explicit AVOpenGLWidget(StreamSession *session, QWidget *parent = nullptr, TransformMode transform_mode = TransformMode::Fit); ~AVOpenGLWidget() override; void SwapFrames(); AVOpenGLFrame *GetBackgroundFrame() { return &frames[1 - frame_fg]; } + void SetTransformMode(TransformMode mode) { transform_mode = mode; } + TransformMode GetTransformMode() const { return transform_mode; } + protected: + TransformMode transform_mode; void mouseMoveEvent(QMouseEvent *event) override; void initializeGL() override; void paintGL() override; - private slots: - void ResetMouseTimeout(); public slots: + void ResetMouseTimeout(); void HideMouse(); }; diff --git a/gui/include/streamsession.h b/gui/include/streamsession.h index cb1397e..e139f45 100644 --- a/gui/include/streamsession.h +++ b/gui/include/streamsession.h @@ -20,6 +20,7 @@ #include "sessionlog.h" #include "controllermanager.h" #include "settings.h" +#include "transformmode.h" #include #include @@ -53,9 +54,17 @@ struct StreamSessionConnectInfo ChiakiConnectVideoProfile video_profile; unsigned int audio_buffer_size; bool fullscreen; + TransformMode transform_mode; bool enable_keyboard; - StreamSessionConnectInfo(Settings *settings, ChiakiTarget target, QString host, QByteArray regist_key, QByteArray morning, bool fullscreen); + StreamSessionConnectInfo( + Settings *settings, + ChiakiTarget target, + QString host, + QByteArray regist_key, + QByteArray morning, + bool fullscreen, + TransformMode transform_mode); }; class StreamSession : public QObject @@ -124,7 +133,7 @@ class StreamSession : public QObject #endif void HandleKeyboardEvent(QKeyEvent *event); - void HandleMouseEvent(QMouseEvent *event); + bool HandleMouseEvent(QMouseEvent *event); signals: void FfmpegFrameAvailable(); diff --git a/gui/include/streamwindow.h b/gui/include/streamwindow.h index 5c526b0..fd91d13 100644 --- a/gui/include/streamwindow.h +++ b/gui/include/streamwindow.h @@ -22,10 +22,14 @@ class StreamWindow: public QMainWindow const StreamSessionConnectInfo connect_info; StreamSession *session; + QAction *fullscreen_action; + QAction *stretch_action; + QAction *zoom_action; AVOpenGLWidget *av_widget; void Init(); void UpdateVideoTransform(); + void UpdateTransformModeActions(); protected: void keyPressEvent(QKeyEvent *event) override; @@ -42,6 +46,8 @@ class StreamWindow: public QMainWindow void SessionQuit(ChiakiQuitReason reason, const QString &reason_str); void LoginPINRequested(bool incorrect); void ToggleFullscreen(); + void ToggleStretch(); + void ToggleZoom(); }; #endif // CHIAKI_GUI_STREAMWINDOW_H diff --git a/gui/include/transformmode.h b/gui/include/transformmode.h new file mode 100644 index 0000000..5c01d87 --- /dev/null +++ b/gui/include/transformmode.h @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: LicenseRef-AGPL-3.0-only-OpenSSL + +#ifndef CHIAKI_TRANSFORMMODE_H +#define CHIAKI_TRANSFORMMODE_H + +enum class TransformMode { + Fit, + Zoom, + Stretch +}; + +#endif diff --git a/gui/src/avopenglwidget.cpp b/gui/src/avopenglwidget.cpp index 857af09..bfd184a 100644 --- a/gui/src/avopenglwidget.cpp +++ b/gui/src/avopenglwidget.cpp @@ -122,9 +122,9 @@ QSurfaceFormat AVOpenGLWidget::CreateSurfaceFormat() return format; } -AVOpenGLWidget::AVOpenGLWidget(StreamSession *session, QWidget *parent) +AVOpenGLWidget::AVOpenGLWidget(StreamSession *session, QWidget *parent, TransformMode transform_mode) : QOpenGLWidget(parent), - session(session) + session(session), transform_mode(transform_mode) { enum AVPixelFormat pixel_format = chiaki_ffmpeg_decoder_get_pixel_format(session->GetFfmpegDecoder()); conversion_config = nullptr; @@ -381,10 +381,10 @@ void AVOpenGLWidget::paintGL() vp_width = widget_width; vp_height = widget_height; } - else + else if(transform_mode == TransformMode::Fit) { float aspect = (float)frame->width / (float)frame->height; - if(aspect < (float)widget_width / (float)widget_height) + if(widget_height && aspect < (float)widget_width / (float)widget_height) { vp_height = widget_height; vp_width = (GLsizei)(vp_height * aspect); @@ -395,6 +395,34 @@ void AVOpenGLWidget::paintGL() vp_height = (GLsizei)(vp_width / aspect); } } + else if(transform_mode == TransformMode::Zoom) + { + float aspect = (float)frame->width / (float)frame->height; + if(widget_height && aspect < (float)widget_width / (float)widget_height) + { + vp_width = widget_width; + vp_height = (GLsizei)(vp_width / aspect); + } + else + { + vp_height = widget_height; + vp_width = (GLsizei)(vp_height * aspect); + } + } + else // transform_mode == TransformMode::Stretch + { + float aspect = (float)frame->width / (float)frame->height; + if(widget_height && aspect < (float)widget_width / (float)widget_height) + { + vp_height = widget_height; + vp_width = widget_width; + } + else + { + vp_width = widget_width; + vp_height = widget_height; + } + } f->glViewport((widget_width - vp_width) / 2, (widget_height - vp_height) / 2, vp_width, vp_height); diff --git a/gui/src/main.cpp b/gui/src/main.cpp index e6597cf..5f2aad7 100644 --- a/gui/src/main.cpp +++ b/gui/src/main.cpp @@ -103,9 +103,15 @@ int real_main(int argc, char *argv[]) QCommandLineOption morning_option("morning", "", "morning"); parser.addOption(morning_option); - QCommandLineOption fullscreen_option("fullscreen", "Start window in fullscreen (only for use with stream command)"); + QCommandLineOption fullscreen_option("fullscreen", "Start window in fullscreen mode [maintains aspect ratio, adds black bars to fill unsused parts of screen if applicable] (only for use with stream command)"); parser.addOption(fullscreen_option); + QCommandLineOption zoom_option("zoom", "Start window in fullscreen zoomed in to fit screen [maintains aspect ratio, cutting off edges of image to fill screen] (only for use with stream command)"); + parser.addOption(zoom_option); + + QCommandLineOption stretch_option("stretch", "Start window in fullscreen stretched to fit screen [distorts aspect ratio to fill screen] (only for use with stream command)"); + parser.addOption(stretch_option); + parser.process(app); QStringList args = parser.positionalArguments(); @@ -174,7 +180,21 @@ int real_main(int argc, char *argv[]) return 1; } } - StreamSessionConnectInfo connect_info(&settings, target, host, regist_key, morning, parser.isSet(fullscreen_option)); + if ((parser.isSet(stretch_option) && (parser.isSet(zoom_option) || parser.isSet(fullscreen_option))) || (parser.isSet(zoom_option) && parser.isSet(fullscreen_option))) + { + printf("Must choose between fullscreen, zoom or stretch option."); + return 1; + } + + StreamSessionConnectInfo connect_info( + &settings, + target, + host, + regist_key, + morning, + parser.isSet(fullscreen_option), + parser.isSet(zoom_option) ? TransformMode::Zoom : parser.isSet(stretch_option) ? TransformMode::Stretch : TransformMode::Fit); + return RunStream(app, connect_info); } #ifdef CHIAKI_ENABLE_CLI diff --git a/gui/src/mainwindow.cpp b/gui/src/mainwindow.cpp index aa76ab3..c3b1b6a 100644 --- a/gui/src/mainwindow.cpp +++ b/gui/src/mainwindow.cpp @@ -249,7 +249,14 @@ void MainWindow::ServerItemWidgetTriggered() } QString host = server.GetHostAddr(); - StreamSessionConnectInfo info(settings, server.registered_host.GetTarget(), host, server.registered_host.GetRPRegistKey(), server.registered_host.GetRPKey(), false); + StreamSessionConnectInfo info( + settings, + server.registered_host.GetTarget(), + host, + server.registered_host.GetRPRegistKey(), + server.registered_host.GetRPKey(), + false, + TransformMode::Fit); new StreamWindow(info); } else diff --git a/gui/src/streamsession.cpp b/gui/src/streamsession.cpp index 3f46070..f5b7ace 100644 --- a/gui/src/streamsession.cpp +++ b/gui/src/streamsession.cpp @@ -14,7 +14,14 @@ #define SETSU_UPDATE_INTERVAL_MS 4 -StreamSessionConnectInfo::StreamSessionConnectInfo(Settings *settings, ChiakiTarget target, QString host, QByteArray regist_key, QByteArray morning, bool fullscreen) +StreamSessionConnectInfo::StreamSessionConnectInfo( + Settings *settings, + ChiakiTarget target, + QString host, + QByteArray regist_key, + QByteArray morning, + bool fullscreen, + TransformMode transform_mode) : settings(settings) { key_map = settings->GetControllerMappingForDecoding(); @@ -30,6 +37,7 @@ StreamSessionConnectInfo::StreamSessionConnectInfo(Settings *settings, ChiakiTar this->morning = morning; audio_buffer_size = settings->GetAudioBufferSize(); this->fullscreen = fullscreen; + this->transform_mode = transform_mode; this->enable_keyboard = false; // TODO: from settings } @@ -228,13 +236,16 @@ void StreamSession::SetLoginPIN(const QString &pin) chiaki_session_set_login_pin(&session, (const uint8_t *)data.constData(), data.size()); } -void StreamSession::HandleMouseEvent(QMouseEvent *event) +bool StreamSession::HandleMouseEvent(QMouseEvent *event) { + if(event->button() != Qt::MouseButton::LeftButton) + return false; if(event->type() == QEvent::MouseButtonPress) keyboard_state.buttons |= CHIAKI_CONTROLLER_BUTTON_TOUCHPAD; else keyboard_state.buttons &= ~CHIAKI_CONTROLLER_BUTTON_TOUCHPAD; SendFeedbackState(); + return true; } void StreamSession::HandleKeyboardEvent(QKeyEvent *event) diff --git a/gui/src/streamwindow.cpp b/gui/src/streamwindow.cpp index feff758..d1fc4e8 100644 --- a/gui/src/streamwindow.cpp +++ b/gui/src/streamwindow.cpp @@ -10,6 +10,7 @@ #include #include #include +#include StreamWindow::StreamWindow(const StreamSessionConnectInfo &connect_info, QWidget *parent) : QMainWindow(parent), @@ -23,8 +24,6 @@ StreamWindow::StreamWindow(const StreamSessionConnectInfo &connect_info, QWidget try { - if(connect_info.fullscreen) - showFullScreen(); Init(); } catch(const Exception &e) @@ -40,6 +39,8 @@ StreamWindow::~StreamWindow() delete av_widget; } +#include + void StreamWindow::Init() { session = new StreamSession(connect_info, this); @@ -47,10 +48,36 @@ void StreamWindow::Init() connect(session, &StreamSession::SessionQuit, this, &StreamWindow::SessionQuit); connect(session, &StreamSession::LoginPINRequested, this, &StreamWindow::LoginPINRequested); + const QKeySequence fullscreen_shortcut = Qt::Key_F11; + const QKeySequence stretch_shortcut = Qt::CTRL + Qt::Key_S; + const QKeySequence zoom_shortcut = Qt::CTRL + Qt::Key_Z; + + fullscreen_action = new QAction(tr("Fullscreen"), this); + fullscreen_action->setCheckable(true); + fullscreen_action->setShortcut(fullscreen_shortcut); + addAction(fullscreen_action); + connect(fullscreen_action, &QAction::triggered, this, &StreamWindow::ToggleFullscreen); + if(session->GetFfmpegDecoder()) { - av_widget = new AVOpenGLWidget(session, this); + av_widget = new AVOpenGLWidget(session, this, connect_info.transform_mode); setCentralWidget(av_widget); + + av_widget->setContextMenuPolicy(Qt::CustomContextMenu); + connect(av_widget, &QWidget::customContextMenuRequested, this, [this](const QPoint &pos) { + av_widget->ResetMouseTimeout(); + + QMenu menu(av_widget); + menu.addAction(fullscreen_action); + menu.addSeparator(); + menu.addAction(stretch_action); + menu.addAction(zoom_action); + releaseKeyboard(); + connect(&menu, &QMenu::aboutToHide, this, [this] { + grabKeyboard(); + }); + menu.exec(av_widget->mapToGlobal(pos)); + }); } else { @@ -63,13 +90,29 @@ void StreamWindow::Init() session->Start(); - auto fullscreen_action = new QAction(tr("Fullscreen"), this); - fullscreen_action->setShortcut(Qt::Key_F11); - addAction(fullscreen_action); - connect(fullscreen_action, &QAction::triggered, this, &StreamWindow::ToggleFullscreen); + stretch_action = new QAction(tr("Stretch"), this); + stretch_action->setCheckable(true); + stretch_action->setShortcut(stretch_shortcut); + addAction(stretch_action); + connect(stretch_action, &QAction::triggered, this, &StreamWindow::ToggleStretch); + + zoom_action = new QAction(tr("Zoom"), this); + zoom_action->setCheckable(true); + zoom_action->setShortcut(zoom_shortcut); + addAction(zoom_action); + connect(zoom_action, &QAction::triggered, this, &StreamWindow::ToggleZoom); resize(connect_info.video_profile.width, connect_info.video_profile.height); - show(); + + if(connect_info.fullscreen) + { + showFullScreen(); + fullscreen_action->setChecked(true); + } + else + show(); + + UpdateTransformModeActions(); } void StreamWindow::keyPressEvent(QKeyEvent *event) @@ -86,20 +129,25 @@ void StreamWindow::keyReleaseEvent(QKeyEvent *event) void StreamWindow::mousePressEvent(QMouseEvent *event) { - if(session) - session->HandleMouseEvent(event); + if(session && session->HandleMouseEvent(event)) + return; + QMainWindow::mousePressEvent(event); } void StreamWindow::mouseReleaseEvent(QMouseEvent *event) { - if(session) - session->HandleMouseEvent(event); + if(session && session->HandleMouseEvent(event)) + return; + QMainWindow::mouseReleaseEvent(event); } void StreamWindow::mouseDoubleClickEvent(QMouseEvent *event) { - ToggleFullscreen(); - + if(event->button() == Qt::MouseButton::LeftButton) + { + ToggleFullscreen(); + return; + } QMainWindow::mouseDoubleClickEvent(event); } @@ -175,15 +223,48 @@ void StreamWindow::LoginPINRequested(bool incorrect) void StreamWindow::ToggleFullscreen() { if(isFullScreen()) + { showNormal(); + fullscreen_action->setChecked(false); + } else { showFullScreen(); if(av_widget) av_widget->HideMouse(); + fullscreen_action->setChecked(true); } } +void StreamWindow::UpdateTransformModeActions() +{ + TransformMode tm = av_widget ? av_widget->GetTransformMode() : TransformMode::Fit; + stretch_action->setChecked(tm == TransformMode::Stretch); + zoom_action->setChecked(tm == TransformMode::Zoom); +} + +void StreamWindow::ToggleStretch() +{ + if(!av_widget) + return; + av_widget->SetTransformMode( + av_widget->GetTransformMode() == TransformMode::Stretch + ? TransformMode::Fit + : TransformMode::Stretch); + UpdateTransformModeActions(); +} + +void StreamWindow::ToggleZoom() +{ + if(!av_widget) + return; + av_widget->SetTransformMode( + av_widget->GetTransformMode() == TransformMode::Zoom + ? TransformMode::Fit + : TransformMode::Zoom); + UpdateTransformModeActions(); +} + void StreamWindow::resizeEvent(QResizeEvent *event) { UpdateVideoTransform(); From 36816db7acd80f88d7070ce9a66cf702f4ad6265 Mon Sep 17 00:00:00 2001 From: Street Pea Date: Sat, 10 Dec 2022 15:12:28 +0100 Subject: [PATCH 221/237] Add quit (Ctrl+Q) shortcut to GUI --- gui/include/mainwindow.h | 1 + gui/include/streamwindow.h | 1 + gui/src/mainwindow.cpp | 10 ++++++++++ gui/src/streamwindow.cpp | 10 ++++++++++ 4 files changed, 22 insertions(+) diff --git a/gui/include/mainwindow.h b/gui/include/mainwindow.h index f1190de..4112c19 100644 --- a/gui/include/mainwindow.h +++ b/gui/include/mainwindow.h @@ -55,6 +55,7 @@ class MainWindow : public QMainWindow void UpdateDiscoveryEnabled(); void ShowSettings(); + void Quit(); void UpdateDisplayServers(); void UpdateServerWidgets(); diff --git a/gui/include/streamwindow.h b/gui/include/streamwindow.h index fd91d13..f3bd8bb 100644 --- a/gui/include/streamwindow.h +++ b/gui/include/streamwindow.h @@ -48,6 +48,7 @@ class StreamWindow: public QMainWindow void ToggleFullscreen(); void ToggleStretch(); void ToggleZoom(); + void Quit(); }; #endif // CHIAKI_GUI_STREAMWINDOW_H diff --git a/gui/src/mainwindow.cpp b/gui/src/mainwindow.cpp index c3b1b6a..0912b2e 100644 --- a/gui/src/mainwindow.cpp +++ b/gui/src/mainwindow.cpp @@ -146,6 +146,11 @@ MainWindow::MainWindow(Settings *settings, QWidget *parent) AddToolBarAction(settings_action); connect(settings_action, &QAction::triggered, this, &MainWindow::ShowSettings); + auto quit_action = new QAction(tr("Quit"), this); + quit_action->setShortcut(Qt::CTRL + Qt::Key_Q); + addAction(quit_action); + connect(quit_action, &QAction::triggered, this, &MainWindow::Quit); + auto scroll_area = new QScrollArea(this); scroll_area->setWidgetResizable(true); scroll_area->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); @@ -305,6 +310,11 @@ void MainWindow::ShowSettings() dialog.exec(); } +void MainWindow::Quit() +{ + qApp->exit(); +} + void MainWindow::UpdateDisplayServers() { display_servers.clear(); diff --git a/gui/src/streamwindow.cpp b/gui/src/streamwindow.cpp index d1fc4e8..d1c0b95 100644 --- a/gui/src/streamwindow.cpp +++ b/gui/src/streamwindow.cpp @@ -102,6 +102,11 @@ void StreamWindow::Init() addAction(zoom_action); connect(zoom_action, &QAction::triggered, this, &StreamWindow::ToggleZoom); + auto quit_action = new QAction(tr("Quit"), this); + quit_action->setShortcut(Qt::CTRL + Qt::Key_Q); + addAction(quit_action); + connect(quit_action, &QAction::triggered, this, &StreamWindow::Quit); + resize(connect_info.video_profile.width, connect_info.video_profile.height); if(connect_info.fullscreen) @@ -127,6 +132,11 @@ void StreamWindow::keyReleaseEvent(QKeyEvent *event) session->HandleKeyboardEvent(event); } +void StreamWindow::Quit() +{ + close(); +} + void StreamWindow::mousePressEvent(QMouseEvent *event) { if(session && session->HandleMouseEvent(event)) From 76690a319cb5d24005561cd5996c59f96ebe2778 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Wed, 14 Dec 2022 18:02:23 +0100 Subject: [PATCH 222/237] Fix testing on AppVeyor and make appveyor-win.sh more portable test/chiaki-unit.exe failed to load some OpenSSL dlls somehow, which broke the build. Moving them next to the executable fixes that. The APPVEYOR_BUILD_FOLDER env var is also not needed anymore now. --- scripts/appveyor-win.sh | 81 ++++++++++++++++++++++++----------------- 1 file changed, 47 insertions(+), 34 deletions(-) diff --git a/scripts/appveyor-win.sh b/scripts/appveyor-win.sh index 3951d84..a7e63a0 100755 --- a/scripts/appveyor-win.sh +++ b/scripts/appveyor-win.sh @@ -1,77 +1,90 @@ #!/bin/bash -echo "APPVEYOR_BUILD_FOLDER=$APPVEYOR_BUILD_FOLDER" +set -xe -mkdir ninja && cd ninja || exit 1 -wget https://github.com/ninja-build/ninja/releases/download/v1.9.0/ninja-win.zip && 7z x ninja-win.zip || exit 1 -cd .. || exit 1 +BUILD_ROOT="$( cd "$( dirname "${BASH_SOURCE[0]}" )/.." && pwd )" +BUILD_ROOT="$(echo $BUILD_ROOT | sed 's|^/\([a-z]\)|\1:|g')" # replace /c/... by c:/... for cmake to understand it +echo "BUILD_ROOT=$BUILD_ROOT" -mkdir yasm && cd yasm || exit 1 -wget http://www.tortall.net/projects/yasm/releases/yasm-1.3.0-win64.exe && mv yasm-1.3.0-win64.exe yasm.exe || exit 1 -cd .. || exit 1 +mkdir ninja && cd ninja +wget https://github.com/ninja-build/ninja/releases/download/v1.9.0/ninja-win.zip && 7z x ninja-win.zip +cd .. + +mkdir yasm && cd yasm +wget http://www.tortall.net/projects/yasm/releases/yasm-1.3.0-win64.exe && mv yasm-1.3.0-win64.exe yasm.exe +cd .. export PATH="$PWD/ninja:$PWD/yasm:/c/Qt/5.12/msvc2017_64/bin:$PATH" -scripts/build-ffmpeg.sh . --target-os=win64 --arch=x86_64 --toolchain=msvc || exit 1 +scripts/build-ffmpeg.sh . --target-os=win64 --arch=x86_64 --toolchain=msvc -git clone https://github.com/xiph/opus.git && cd opus && git checkout ad8fe90db79b7d2a135e3dfd2ed6631b0c5662ab || exit 1 -mkdir build && cd build || exit 1 +git clone https://github.com/xiph/opus.git && cd opus && git checkout ad8fe90db79b7d2a135e3dfd2ed6631b0c5662ab +mkdir build && cd build cmake \ -G Ninja \ -DCMAKE_C_COMPILER=cl \ -DCMAKE_BUILD_TYPE=Release \ - -DCMAKE_INSTALL_PREFIX="$APPVEYOR_BUILD_FOLDER/opus-prefix" \ - .. || exit 1 -ninja || exit 1 -ninja install || exit 1 -cd ../.. || exit 1 + -DCMAKE_INSTALL_PREFIX="$BUILD_ROOT/opus-prefix" \ + .. +ninja +ninja install +cd ../.. -wget https://mirror.firedaemon.com/OpenSSL/openssl-1.1.1q.zip && 7z x openssl-1.1.1q.zip || exit 1 +wget https://mirror.firedaemon.com/OpenSSL/openssl-1.1.1q.zip && 7z x openssl-1.1.1q.zip -wget https://www.libsdl.org/release/SDL2-devel-2.0.10-VC.zip && 7z x SDL2-devel-2.0.10-VC.zip || exit 1 -export SDL_ROOT="$APPVEYOR_BUILD_FOLDER/SDL2-2.0.10" || exit 1 -export SDL_ROOT=${SDL_ROOT//[\\]//} || exit 1 +wget https://www.libsdl.org/release/SDL2-devel-2.0.10-VC.zip && 7z x SDL2-devel-2.0.10-VC.zip +export SDL_ROOT="$BUILD_ROOT/SDL2-2.0.10" +export SDL_ROOT=${SDL_ROOT//[\\]//} echo "set(SDL2_INCLUDE_DIRS \"$SDL_ROOT/include\") set(SDL2_LIBRARIES \"$SDL_ROOT/lib/x64/SDL2.lib\") -set(SDL2_LIBDIR \"$SDL_ROOT/lib/x64\")" > "$SDL_ROOT/SDL2Config.cmake" || exit 1 +set(SDL2_LIBDIR \"$SDL_ROOT/lib/x64\")" > "$SDL_ROOT/SDL2Config.cmake" -mkdir protoc && cd protoc || exit 1 -wget https://github.com/protocolbuffers/protobuf/releases/download/v3.9.1/protoc-3.9.1-win64.zip && 7z x protoc-3.9.1-win64.zip || exit 1 -cd .. || exit 1 -export PATH="$PWD/protoc/bin:$PATH" || exit 1 +mkdir protoc && cd protoc +wget https://github.com/protocolbuffers/protobuf/releases/download/v3.9.1/protoc-3.9.1-win64.zip && 7z x protoc-3.9.1-win64.zip +cd .. +export PATH="$PWD/protoc/bin:$PATH" PYTHON="C:/Python37/python.exe" -"$PYTHON" -m pip install protobuf==3.19.5 || exit 1 +"$PYTHON" -m pip install protobuf==3.19.5 QT_PATH="C:/Qt/5.15/msvc2019_64" COPY_DLLS="$PWD/openssl-1.1/x64/bin/libcrypto-1_1-x64.dll $PWD/openssl-1.1/x64/bin/libssl-1_1-x64.dll $SDL_ROOT/lib/x64/SDL2.dll" -mkdir build && cd build || exit 1 +echo "-- Configure" + +mkdir build && cd build cmake \ -G Ninja \ -DCMAKE_C_COMPILER=cl \ -DCMAKE_C_FLAGS="-we4013" \ -DCMAKE_BUILD_TYPE=RelWithDebInfo \ - -DCMAKE_PREFIX_PATH="$APPVEYOR_BUILD_FOLDER/ffmpeg-prefix;$APPVEYOR_BUILD_FOLDER/opus-prefix;$APPVEYOR_BUILD_FOLDER/openssl-1.1/x64;$QT_PATH;$SDL_ROOT" \ + -DCMAKE_PREFIX_PATH="$BUILD_ROOT/ffmpeg-prefix;$BUILD_ROOT/opus-prefix;$BUILD_ROOT/openssl-1.1/x64;$QT_PATH;$SDL_ROOT" \ -DPYTHON_EXECUTABLE="$PYTHON" \ -DCHIAKI_ENABLE_TESTS=ON \ -DCHIAKI_ENABLE_CLI=OFF \ -DCHIAKI_GUI_ENABLE_SDL_GAMECONTROLLER=ON \ - .. || exit 1 + .. -ninja || exit 1 +echo "-- Build" -test/chiaki-unit.exe || exit 1 +ninja -cd .. || exit 1 +echo "-- Test" + +cp $COPY_DLLS test/ +test/chiaki-unit.exe + +cd .. # Deploy -mkdir Chiaki && cp build/gui/chiaki.exe Chiaki || exit 1 -mkdir Chiaki-PDB && cp build/gui/chiaki.pdb Chiaki-PDB || exit 1 +echo "-- Deploy" -"$QT_PATH/bin/windeployqt.exe" Chiaki/chiaki.exe || exit 1 +mkdir Chiaki && cp build/gui/chiaki.exe Chiaki +mkdir Chiaki-PDB && cp build/gui/chiaki.pdb Chiaki-PDB + +"$QT_PATH/bin/windeployqt.exe" Chiaki/chiaki.exe cp -v $COPY_DLLS Chiaki From 4c8209762c1f822d2066367062170f47692abd54 Mon Sep 17 00:00:00 2001 From: Johannes Baiter Date: Mon, 7 Nov 2022 22:50:32 +0100 Subject: [PATCH 223/237] Add support for touchpad and sensor handling via SDL This should enable support for more controllers besides the DS4 and DualSense, basically any controller supported by SDL that has at least one touchpad, an accelerometer and a gyroscope. Older SDL versions have been tested down to 2.0.9. Versions older than 2.0.14 won't have sensors and touchpad support, though. Setsu is deprecated and remains in-tree for now, but defaults to being disabled if SDL2 is found and >= 2.0.14. If Setsu is enabled explicitly, touchpad and sensors are not handled by SDL. --- CMakeLists.txt | 36 ++-- cmake/FindSDL2.cmake | 20 +++ gui/CMakeLists.txt | 3 - gui/include/controllermanager.h | 21 ++- gui/src/controllermanager.cpp | 254 +++++++++++++++++++++++---- lib/src/controller.c | 13 ++ scripts/build-appimage.sh | 1 - scripts/build-sdl2.sh | 7 +- scripts/run-podman-build-bullseye.sh | 2 +- 9 files changed, 298 insertions(+), 59 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 3a552a0..3af7f5c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -150,21 +150,30 @@ if(CHIAKI_ENABLE_CLI) add_subdirectory(cli) endif() +if(CHIAKI_ENABLE_GUI AND CHIAKI_GUI_ENABLE_SDL_GAMECONTROLLER) + find_package(SDL2 MODULE REQUIRED) +endif() + if(CHIAKI_ENABLE_SETSU) - find_package(Udev QUIET) - find_package(Evdev QUIET) - if(Udev_FOUND AND Evdev_FOUND) - set(CHIAKI_ENABLE_SETSU ON) - else() - if(NOT CHIAKI_ENABLE_SETSU STREQUAL AUTO) - message(FATAL_ERROR " -CHIAKI_ENABLE_SETSU is set to ON, but its dependencies (udev and evdev) could not be resolved. -Keep in mind that setsu is only supported on Linux!") - endif() + if(CHIAKI_ENABLE_SETSU STREQUAL AUTO AND SDL2_FOUND AND (SDL2_VERSION_MINOR GREATER 0 OR SDL2_VERSION_PATCH GREATER_EQUAL 14)) + message(STATUS "SDL version ${SDL2_VERSION} is >= 2.0.14, disabling Setsu") set(CHIAKI_ENABLE_SETSU OFF) - endif() - if(CHIAKI_ENABLE_SETSU) - add_subdirectory(setsu) + else() + find_package(Udev QUIET) + find_package(Evdev QUIET) + if(Udev_FOUND AND Evdev_FOUND) + set(CHIAKI_ENABLE_SETSU ON) + else() + if(NOT CHIAKI_ENABLE_SETSU STREQUAL AUTO) + message(FATAL_ERROR " + CHIAKI_ENABLE_SETSU is set to ON, but its dependencies (udev and evdev) could not be resolved. + Keep in mind that setsu is only supported on Linux!") + endif() + set(CHIAKI_ENABLE_SETSU OFF) + endif() + if(CHIAKI_ENABLE_SETSU) + add_subdirectory(setsu) + endif() endif() endif() @@ -175,7 +184,6 @@ else() endif() if(CHIAKI_ENABLE_GUI) - #add_subdirectory(setsu) add_subdirectory(gui) endif() diff --git a/cmake/FindSDL2.cmake b/cmake/FindSDL2.cmake index a0b9ebd..03717b0 100644 --- a/cmake/FindSDL2.cmake +++ b/cmake/FindSDL2.cmake @@ -1,6 +1,26 @@ find_package(SDL2 NO_MODULE QUIET) +# Adapted from libsdl-org/SDL_ttf: https://github.com/libsdl-org/SDL_ttf/blob/main/cmake/FindPrivateSDL2.cmake#L19-L31 +# Copyright (C) 1997-2022 Sam Lantinga +# Licensed under the zlib license (https://github.com/libsdl-org/SDL_ttf/blob/main/LICENSE.txt) +set(SDL2_VERSION_MAJOR) +set(SDL2_VERSION_MINOR) +set(SDL2_VERSION_PATCH) +set(SDL2_VERSION) +if(SDL2_INCLUDE_DIR) + file(READ "${SDL2_INCLUDE_DIR}/SDL_version.h" _sdl_version_h) + string(REGEX MATCH "#define[ \t]+SDL_MAJOR_VERSION[ \t]+([0-9]+)" _sdl2_major_re "${_sdl_version_h}") + set(SDL2_VERSION_MAJOR "${CMAKE_MATCH_1}") + string(REGEX MATCH "#define[ \t]+SDL_MINOR_VERSION[ \t]+([0-9]+)" _sdl2_minor_re "${_sdl_version_h}") + set(SDL2_VERSION_MINOR "${CMAKE_MATCH_1}") + string(REGEX MATCH "#define[ \t]+SDL_PATCHLEVEL[ \t]+([0-9]+)" _sdl2_patch_re "${_sdl_version_h}") + set(SDL2_VERSION_PATCH "${CMAKE_MATCH_1}") + if(_sdl2_major_re AND _sdl2_minor_re AND _sdl2_patch_re) + set(SDL2_VERSION "${SDL2_VERSION_MAJOR}.${SDL2_VERSION_MINOR}.${SDL2_VERSION_PATCH}") + endif() +endif() + if(SDL2_FOUND AND (NOT TARGET SDL2::SDL2)) add_library(SDL2::SDL2 UNKNOWN IMPORTED GLOBAL) if(NOT SDL2_LIBDIR) diff --git a/gui/CMakeLists.txt b/gui/CMakeLists.txt index 5f44908..5e51b55 100644 --- a/gui/CMakeLists.txt +++ b/gui/CMakeLists.txt @@ -6,9 +6,6 @@ find_package(Qt5 REQUIRED COMPONENTS Core Widgets Gui Concurrent Multimedia Open if(APPLE) find_package(Qt5 REQUIRED COMPONENTS MacExtras) endif() -if(CHIAKI_GUI_ENABLE_SDL_GAMECONTROLLER) - find_package(SDL2 MODULE REQUIRED) -endif() if(WIN32) add_definitions(-DWIN32_LEAN_AND_MEAN) diff --git a/gui/include/controllermanager.h b/gui/include/controllermanager.h index 18d9d72..9e89fcc 100644 --- a/gui/include/controllermanager.h +++ b/gui/include/controllermanager.h @@ -12,8 +12,12 @@ #ifdef CHIAKI_GUI_ENABLE_SDL_GAMECONTROLLER #include +#include #endif +#define PS_TOUCHPAD_MAX_X 1920 +#define PS_TOUCHPAD_MAX_Y 1079 + class Controller; class ControllerManager : public QObject @@ -33,7 +37,9 @@ class ControllerManager : public QObject private slots: void UpdateAvailableControllers(); void HandleEvents(); - void ControllerEvent(int device_id); +#ifdef CHIAKI_GUI_ENABLE_SDL_GAMECONTROLLER + void ControllerEvent(SDL_Event evt); +#endif public: static ControllerManager *GetInstance(); @@ -57,12 +63,23 @@ class Controller : public QObject private: Controller(int device_id, ControllerManager *manager); - void UpdateState(); +#ifdef CHIAKI_GUI_ENABLE_SDL_GAMECONTROLLER + void UpdateState(SDL_Event event); + bool HandleButtonEvent(SDL_ControllerButtonEvent event); + bool HandleAxisEvent(SDL_ControllerAxisEvent event); +#if SDL_VERSION_ATLEAST(2, 0, 14) + bool HandleSensorEvent(SDL_ControllerSensorEvent event); + bool HandleTouchpadEvent(SDL_ControllerTouchpadEvent event); +#endif +#endif ControllerManager *manager; int id; + ChiakiOrientationTracker orientation_tracker; + ChiakiControllerState state; #ifdef CHIAKI_GUI_ENABLE_SDL_GAMECONTROLLER + QMap, uint8_t> touch_ids; SDL_GameController *controller; #endif diff --git a/gui/src/controllermanager.cpp b/gui/src/controllermanager.cpp index 667b736..b1f38e7 100644 --- a/gui/src/controllermanager.cpp +++ b/gui/src/controllermanager.cpp @@ -156,22 +156,51 @@ void ControllerManager::HandleEvents() break; case SDL_CONTROLLERBUTTONUP: case SDL_CONTROLLERBUTTONDOWN: - ControllerEvent(event.cbutton.which); - break; case SDL_CONTROLLERAXISMOTION: - ControllerEvent(event.caxis.which); +#if not defined(CHIAKI_ENABLE_SETSU) and SDL_VERSION_ATLEAST(2, 0, 14) + case SDL_CONTROLLERSENSORUPDATE: + case SDL_CONTROLLERTOUCHPADDOWN: + case SDL_CONTROLLERTOUCHPADMOTION: + case SDL_CONTROLLERTOUCHPADUP: +#endif + ControllerEvent(event); break; } } #endif } -void ControllerManager::ControllerEvent(int device_id) +#ifdef CHIAKI_GUI_ENABLE_SDL_GAMECONTROLLER +void ControllerManager::ControllerEvent(SDL_Event event) { + int device_id; + switch(event.type) + { + case SDL_CONTROLLERBUTTONDOWN: + case SDL_CONTROLLERBUTTONUP: + device_id = event.cbutton.which; + break; + case SDL_CONTROLLERAXISMOTION: + device_id = event.caxis.which; + break; +#if SDL_VERSION_ATLEAST(2, 0, 14) + case SDL_CONTROLLERSENSORUPDATE: + device_id = event.csensor.which; + break; + case SDL_CONTROLLERTOUCHPADDOWN: + case SDL_CONTROLLERTOUCHPADMOTION: + case SDL_CONTROLLERTOUCHPADUP: + device_id = event.ctouchpad.which; + break; +#endif + default: + return; + } if(!open_controllers.contains(device_id)) return; - open_controllers[device_id]->UpdateState(); + open_controllers[device_id]->UpdateState(event); } +#endif QSet ControllerManager::GetAvailableControllers() { @@ -200,6 +229,8 @@ Controller::Controller(int device_id, ControllerManager *manager) : QObject(mana { this->id = device_id; this->manager = manager; + chiaki_orientation_tracker_init(&this->orientation_tracker); + chiaki_controller_state_set_idle(&this->state); #ifdef CHIAKI_GUI_ENABLE_SDL_GAMECONTROLLER controller = nullptr; @@ -208,7 +239,13 @@ Controller::Controller(int device_id, ControllerManager *manager) : QObject(mana if(SDL_JoystickGetDeviceInstanceID(i) == device_id) { controller = SDL_GameControllerOpen(i); +#if SDL_VERSION_ATLEAST(2, 0, 14) + if(SDL_GameControllerHasSensor(controller, SDL_SENSOR_ACCEL)) + SDL_GameControllerSetSensorEnabled(controller, SDL_SENSOR_ACCEL, SDL_TRUE); + if(SDL_GameControllerHasSensor(controller, SDL_SENSOR_GYRO)) + SDL_GameControllerSetSensorEnabled(controller, SDL_SENSOR_GYRO, SDL_TRUE); break; +#endif } } #endif @@ -223,11 +260,187 @@ Controller::~Controller() manager->ControllerClosed(this); } -void Controller::UpdateState() +#ifdef CHIAKI_GUI_ENABLE_SDL_GAMECONTROLLER +void Controller::UpdateState(SDL_Event event) { + switch(event.type) + { + case SDL_CONTROLLERBUTTONDOWN: + case SDL_CONTROLLERBUTTONUP: + if(!HandleButtonEvent(event.cbutton)) + return; + break; + case SDL_CONTROLLERAXISMOTION: + if(!HandleAxisEvent(event.caxis)) + return; + break; +#if SDL_VERSION_ATLEAST(2, 0, 14) + case SDL_CONTROLLERSENSORUPDATE: + if(!HandleSensorEvent(event.csensor)) + return; + break; + case SDL_CONTROLLERTOUCHPADDOWN: + case SDL_CONTROLLERTOUCHPADMOTION: + case SDL_CONTROLLERTOUCHPADUP: + if(!HandleTouchpadEvent(event.ctouchpad)) + return; + break; +#endif + default: + return; + + } emit StateChanged(); } +inline bool Controller::HandleButtonEvent(SDL_ControllerButtonEvent event) { + ChiakiControllerButton ps_btn; + switch(event.button) + { + case SDL_CONTROLLER_BUTTON_A: + ps_btn = CHIAKI_CONTROLLER_BUTTON_CROSS; + break; + case SDL_CONTROLLER_BUTTON_B: + ps_btn = CHIAKI_CONTROLLER_BUTTON_MOON; + break; + case SDL_CONTROLLER_BUTTON_X: + ps_btn = CHIAKI_CONTROLLER_BUTTON_BOX; + break; + case SDL_CONTROLLER_BUTTON_Y: + ps_btn = CHIAKI_CONTROLLER_BUTTON_PYRAMID; + break; + case SDL_CONTROLLER_BUTTON_DPAD_LEFT: + ps_btn = CHIAKI_CONTROLLER_BUTTON_DPAD_LEFT; + break; + case SDL_CONTROLLER_BUTTON_DPAD_RIGHT: + ps_btn = CHIAKI_CONTROLLER_BUTTON_DPAD_RIGHT; + break; + case SDL_CONTROLLER_BUTTON_DPAD_UP: + ps_btn = CHIAKI_CONTROLLER_BUTTON_DPAD_UP; + break; + case SDL_CONTROLLER_BUTTON_DPAD_DOWN: + ps_btn = CHIAKI_CONTROLLER_BUTTON_DPAD_DOWN; + break; + case SDL_CONTROLLER_BUTTON_LEFTSHOULDER: + ps_btn = CHIAKI_CONTROLLER_BUTTON_L1; + break; + case SDL_CONTROLLER_BUTTON_RIGHTSHOULDER: + ps_btn = CHIAKI_CONTROLLER_BUTTON_R1; + break; + case SDL_CONTROLLER_BUTTON_LEFTSTICK: + ps_btn = CHIAKI_CONTROLLER_BUTTON_L3; + break; + case SDL_CONTROLLER_BUTTON_RIGHTSTICK: + ps_btn = CHIAKI_CONTROLLER_BUTTON_R3; + break; + case SDL_CONTROLLER_BUTTON_START: + ps_btn = CHIAKI_CONTROLLER_BUTTON_OPTIONS; + break; + case SDL_CONTROLLER_BUTTON_BACK: + ps_btn = CHIAKI_CONTROLLER_BUTTON_SHARE; + break; + case SDL_CONTROLLER_BUTTON_GUIDE: + ps_btn = CHIAKI_CONTROLLER_BUTTON_PS; + break; +#if SDL_VERSION_ATLEAST(2, 0, 14) + case SDL_CONTROLLER_BUTTON_TOUCHPAD: + ps_btn = CHIAKI_CONTROLLER_BUTTON_TOUCHPAD; + break; +#endif + default: + return false; + } + if(event.type == SDL_CONTROLLERBUTTONDOWN) + state.buttons |= ps_btn; + else + state.buttons &= ~ps_btn; + return true; +} + +inline bool Controller::HandleAxisEvent(SDL_ControllerAxisEvent event) { + switch(event.axis) + { + case SDL_CONTROLLER_AXIS_TRIGGERLEFT: + state.l2_state = (uint8_t)(event.value >> 7); + break; + case SDL_CONTROLLER_AXIS_TRIGGERRIGHT: + state.r2_state = (uint8_t)(event.value >> 7); + break; + case SDL_CONTROLLER_AXIS_LEFTX: + state.left_x = event.value; + break; + case SDL_CONTROLLER_AXIS_LEFTY: + state.left_y = event.value; + break; + case SDL_CONTROLLER_AXIS_RIGHTX: + state.right_x = event.value; + break; + case SDL_CONTROLLER_AXIS_RIGHTY: + state.right_y = event.value; + break; + default: + return false; + } + return true; +} + +#if SDL_VERSION_ATLEAST(2, 0, 14) +inline bool Controller::HandleSensorEvent(SDL_ControllerSensorEvent event) +{ + switch(event.sensor) + { + case SDL_SENSOR_ACCEL: + state.accel_x = event.data[0] / SDL_STANDARD_GRAVITY; + state.accel_y = event.data[1] / SDL_STANDARD_GRAVITY; + state.accel_z = event.data[2] / SDL_STANDARD_GRAVITY; + break; + case SDL_SENSOR_GYRO: + state.gyro_x = event.data[0]; + state.gyro_y = event.data[1]; + state.gyro_z = event.data[2]; + break; + default: + return false; + } + chiaki_orientation_tracker_update( + &orientation_tracker, state.gyro_x, state.gyro_y, state.gyro_z, + state.accel_x, state.accel_y, state.accel_z, event.timestamp * 1000); + chiaki_orientation_tracker_apply_to_controller_state(&orientation_tracker, &state); + return true; +} + +inline bool Controller::HandleTouchpadEvent(SDL_ControllerTouchpadEvent event) +{ + auto key = qMakePair(event.touchpad, event.finger); + bool exists = touch_ids.contains(key); + uint8_t chiaki_id; + switch(event.type) + { + case SDL_CONTROLLERTOUCHPADDOWN: + if(touch_ids.size() >= CHIAKI_CONTROLLER_TOUCHES_MAX) + return false; + chiaki_id = chiaki_controller_state_start_touch(&state, event.x * PS_TOUCHPAD_MAX_X, event.y * PS_TOUCHPAD_MAX_Y); + touch_ids.insert(key, chiaki_id); + break; + case SDL_CONTROLLERTOUCHPADMOTION: + if(!exists) + return false; + chiaki_controller_state_set_touch_pos(&state, touch_ids[key], event.x * PS_TOUCHPAD_MAX_X, event.y * PS_TOUCHPAD_MAX_Y); + break; + case SDL_CONTROLLERTOUCHPADUP: + if(!exists) + return false; + chiaki_controller_state_stop_touch(&state, touch_ids[key]); + touch_ids.remove(key); + break; + default: + return false; + } + return true; +} +#endif +#endif + bool Controller::IsConnected() { #ifdef CHIAKI_GUI_ENABLE_SDL_GAMECONTROLLER @@ -263,35 +476,6 @@ QString Controller::GetName() ChiakiControllerState Controller::GetState() { - ChiakiControllerState state; - chiaki_controller_state_set_idle(&state); -#ifdef CHIAKI_GUI_ENABLE_SDL_GAMECONTROLLER - if(!controller) - return state; - - state.buttons |= SDL_GameControllerGetButton(controller, SDL_CONTROLLER_BUTTON_A) ? CHIAKI_CONTROLLER_BUTTON_CROSS : 0; - state.buttons |= SDL_GameControllerGetButton(controller, SDL_CONTROLLER_BUTTON_B) ? CHIAKI_CONTROLLER_BUTTON_MOON : 0; - state.buttons |= SDL_GameControllerGetButton(controller, SDL_CONTROLLER_BUTTON_X) ? CHIAKI_CONTROLLER_BUTTON_BOX : 0; - state.buttons |= SDL_GameControllerGetButton(controller, SDL_CONTROLLER_BUTTON_Y) ? CHIAKI_CONTROLLER_BUTTON_PYRAMID : 0; - state.buttons |= SDL_GameControllerGetButton(controller, SDL_CONTROLLER_BUTTON_DPAD_LEFT) ? CHIAKI_CONTROLLER_BUTTON_DPAD_LEFT : 0; - state.buttons |= SDL_GameControllerGetButton(controller, SDL_CONTROLLER_BUTTON_DPAD_RIGHT) ? CHIAKI_CONTROLLER_BUTTON_DPAD_RIGHT : 0; - state.buttons |= SDL_GameControllerGetButton(controller, SDL_CONTROLLER_BUTTON_DPAD_UP) ? CHIAKI_CONTROLLER_BUTTON_DPAD_UP : 0; - state.buttons |= SDL_GameControllerGetButton(controller, SDL_CONTROLLER_BUTTON_DPAD_DOWN) ? CHIAKI_CONTROLLER_BUTTON_DPAD_DOWN : 0; - state.buttons |= SDL_GameControllerGetButton(controller, SDL_CONTROLLER_BUTTON_LEFTSHOULDER) ? CHIAKI_CONTROLLER_BUTTON_L1 : 0; - state.buttons |= SDL_GameControllerGetButton(controller, SDL_CONTROLLER_BUTTON_RIGHTSHOULDER) ? CHIAKI_CONTROLLER_BUTTON_R1 : 0; - state.buttons |= SDL_GameControllerGetButton(controller, SDL_CONTROLLER_BUTTON_LEFTSTICK) ? CHIAKI_CONTROLLER_BUTTON_L3 : 0; - state.buttons |= SDL_GameControllerGetButton(controller, SDL_CONTROLLER_BUTTON_RIGHTSTICK) ? CHIAKI_CONTROLLER_BUTTON_R3 : 0; - state.buttons |= SDL_GameControllerGetButton(controller, SDL_CONTROLLER_BUTTON_START) ? CHIAKI_CONTROLLER_BUTTON_OPTIONS : 0; - state.buttons |= SDL_GameControllerGetButton(controller, SDL_CONTROLLER_BUTTON_BACK) ? CHIAKI_CONTROLLER_BUTTON_SHARE : 0; - state.buttons |= SDL_GameControllerGetButton(controller, SDL_CONTROLLER_BUTTON_GUIDE) ? CHIAKI_CONTROLLER_BUTTON_PS : 0; - state.l2_state = (uint8_t)(SDL_GameControllerGetAxis(controller, SDL_CONTROLLER_AXIS_TRIGGERLEFT) >> 7); - state.r2_state = (uint8_t)(SDL_GameControllerGetAxis(controller, SDL_CONTROLLER_AXIS_TRIGGERRIGHT) >> 7); - state.left_x = SDL_GameControllerGetAxis(controller, SDL_CONTROLLER_AXIS_LEFTX); - state.left_y = SDL_GameControllerGetAxis(controller, SDL_CONTROLLER_AXIS_LEFTY); - state.right_x = SDL_GameControllerGetAxis(controller, SDL_CONTROLLER_AXIS_RIGHTX); - state.right_y = SDL_GameControllerGetAxis(controller, SDL_CONTROLLER_AXIS_RIGHTY); - -#endif return state; } diff --git a/lib/src/controller.c b/lib/src/controller.c index 21b9971..744a83f 100644 --- a/lib/src/controller.c +++ b/lib/src/controller.c @@ -121,6 +121,19 @@ CHIAKI_EXPORT void chiaki_controller_state_or(ChiakiControllerState *out, Chiaki out->right_x = MAX_ABS(a->right_x, b->right_x); out->right_y = MAX_ABS(a->right_y, b->right_y); + #define ORF(n, idle_val) if(a->n == idle_val) out->n = b->n; else out->n = a->n + ORF(accel_x, 0.0f); + ORF(accel_y, 1.0f); + ORF(accel_z, 0.0f); + ORF(gyro_x, 0.0f); + ORF(gyro_y, 0.0f); + ORF(gyro_z, 0.0f); + ORF(orient_x, 0.0f); + ORF(orient_y, 0.0f); + ORF(orient_z, 0.0f); + ORF(orient_w, 1.0f); + #undef ORF + out->touch_id_next = 0; for(size_t i = 0; i < CHIAKI_CONTROLLER_TOUCHES_MAX; i++) { diff --git a/scripts/build-appimage.sh b/scripts/build-appimage.sh index 83903fd..4fbd588 100755 --- a/scripts/build-appimage.sh +++ b/scripts/build-appimage.sh @@ -22,7 +22,6 @@ cmake \ -DCHIAKI_ENABLE_TESTS=ON \ -DCHIAKI_ENABLE_CLI=OFF \ -DCHIAKI_ENABLE_GUI=ON \ - -DCHIAKI_ENABLE_SETSU=ON \ -DCHIAKI_GUI_ENABLE_SDL_GAMECONTROLLER=ON \ -DCMAKE_INSTALL_PREFIX=/usr \ .. diff --git a/scripts/build-sdl2.sh b/scripts/build-sdl2.sh index f06b9cc..725bbf4 100755 --- a/scripts/build-sdl2.sh +++ b/scripts/build-sdl2.sh @@ -6,9 +6,10 @@ cd $(dirname "${BASH_SOURCE[0]}")/.. cd "./$1" ROOT="`pwd`" -URL=https://www.libsdl.org/release/SDL2-2.0.10.tar.gz -FILE=SDL2-2.0.10.tar.gz -DIR=SDL2-2.0.10 +SDL_VER=2.26.1 +URL=https://www.libsdl.org/release/SDL2-${SDL_VER}.tar.gz +FILE=SDL2-${SDL_VER}.tar.gz +DIR=SDL2-${SDL_VER} if [ ! -d "$DIR" ]; then curl -L "$URL" -O diff --git a/scripts/run-podman-build-bullseye.sh b/scripts/run-podman-build-bullseye.sh index ad5c6d1..344a76e 100755 --- a/scripts/run-podman-build-bullseye.sh +++ b/scripts/run-podman-build-bullseye.sh @@ -9,7 +9,7 @@ podman run --rm -v "`pwd`:/build" chiaki-bullseye /bin/bash -c " cd /build && rm -fv third-party/nanopb/generator/proto/nanopb_pb2.py && mkdir build_bullseye && - cmake -Bbuild_bullseye -GNinja -DCHIAKI_ENABLE_SETSU=ON -DCHIAKI_USE_SYSTEM_JERASURE=ON -DCHIAKI_USE_SYSTEM_NANOPB=ON && + cmake -Bbuild_bullseye -GNinja -DCHIAKI_USE_SYSTEM_JERASURE=ON -DCHIAKI_USE_SYSTEM_NANOPB=ON && ninja -C build_bullseye && ninja -C build_bullseye test" From 7a490b5aaea1d8cd26e042eb9f681d5a05df98c5 Mon Sep 17 00:00:00 2001 From: Johannes Baiter Date: Mon, 21 Nov 2022 20:21:43 +0100 Subject: [PATCH 224/237] Fix feedback state position 0x1b when DualSense is connected. The previous value of `0` caused the PS5 to expect a set of 'trigger status' values in the 0x19 and 0x1a position, which would have required reading raw values from the DualSense HID device, since these values are not reported by the SDL DualSense driver (code that does so can be checked out from the `trigger-feedback` branch on https://git.sr.ht/~jbaiter/chiaki). Fortunately this is not neccessary, simply setting the value to `1` seems to make the PS5 to rely on fallback logic (presumably based on the L2/R2 value) and games that would otherwise have relied on the trigger status (Astro's Playroom climbing levels) now work without any problems. --- lib/include/chiaki/feedback.h | 2 +- lib/src/feedback.c | 8 ++++++-- lib/src/takion.c | 2 +- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/lib/include/chiaki/feedback.h b/lib/include/chiaki/feedback.h index d0be7a1..be66384 100644 --- a/lib/include/chiaki/feedback.h +++ b/lib/include/chiaki/feedback.h @@ -38,7 +38,7 @@ CHIAKI_EXPORT void chiaki_feedback_state_format_v9(uint8_t *buf, ChiakiFeedbackS /** * @param buf buffer of at least CHIAKI_FEEDBACK_STATE_BUF_SIZE_V12 */ -CHIAKI_EXPORT void chiaki_feedback_state_format_v12(uint8_t *buf, ChiakiFeedbackState *state, bool enable_dualsense); +CHIAKI_EXPORT void chiaki_feedback_state_format_v12(uint8_t *buf, ChiakiFeedbackState *state); #define CHIAKI_HISTORY_EVENT_SIZE_MAX 0x5 diff --git a/lib/src/feedback.c b/lib/src/feedback.c index d585a88..6db8364 100644 --- a/lib/src/feedback.c +++ b/lib/src/feedback.c @@ -76,12 +76,16 @@ CHIAKI_EXPORT void chiaki_feedback_state_format_v9(uint8_t *buf, ChiakiFeedbackS *((chiaki_unaligned_uint16_t *)(buf + 0x17)) = htons((uint16_t)state->right_y); } -CHIAKI_EXPORT void chiaki_feedback_state_format_v12(uint8_t *buf, ChiakiFeedbackState *state, bool enable_dualsense) +CHIAKI_EXPORT void chiaki_feedback_state_format_v12(uint8_t *buf, ChiakiFeedbackState *state) { chiaki_feedback_state_format_v9(buf, state); buf[0x19] = 0x0; buf[0x1a] = 0x0; - buf[0x1b] = enable_dualsense ? 0x0 : 0x1; + + // 1 is classic DualShock, 0 is DualSense, but using 0 requires setting [0x19] and [0x1a] to + // values taken from raw HID, which is generally not available. But setting 1 for both seems + // to always work fine. + buf[0x1b] = 0x1; } CHIAKI_EXPORT ChiakiErrorCode chiaki_feedback_history_event_set_button(ChiakiFeedbackHistoryEvent *event, uint64_t button, uint8_t state) diff --git a/lib/src/takion.c b/lib/src/takion.c index f786d6f..baae28c 100644 --- a/lib/src/takion.c +++ b/lib/src/takion.c @@ -558,7 +558,7 @@ CHIAKI_EXPORT ChiakiErrorCode chiaki_takion_send_feedback_state(ChiakiTakion *ta else { buf_sz = 0xc + CHIAKI_FEEDBACK_STATE_BUF_SIZE_V12; - chiaki_feedback_state_format_v12(buf + 0xc, feedback_state, takion->enable_dualsense); + chiaki_feedback_state_format_v12(buf + 0xc, feedback_state); } return takion_send_feedback_packet(takion, buf, buf_sz); } From c2f09326702a723c3365c49a25d30047beecb154 Mon Sep 17 00:00:00 2001 From: Johannes Baiter Date: Tue, 1 Nov 2022 10:37:20 +0100 Subject: [PATCH 225/237] gui: Support for DualSense haptics and trigger effects Haptics with PulseAudio does not seem to be working properly, so using Pipewire as a backend is recommended (and picked by default, if available via an SDL hint). --- .appveyor.yml | 2 +- gui/CMakeLists.txt | 2 +- gui/include/controllermanager.h | 35 ++++++ gui/include/settings.h | 3 + gui/include/settingsdialog.h | 2 + gui/include/streamsession.h | 7 ++ gui/src/controllermanager.cpp | 52 ++++++++- gui/src/settingsdialog.cpp | 10 ++ gui/src/streamsession.cpp | 167 ++++++++++++++++++++++++++++- scripts/appveyor-win.sh | 10 +- scripts/build-sdl2.sh | 4 +- scripts/kitware-archive-latest.asc | 120 ++++++++++----------- 12 files changed, 343 insertions(+), 71 deletions(-) diff --git a/.appveyor.yml b/.appveyor.yml index 7dc4799..db3c3a0 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -37,7 +37,7 @@ for: install: - git submodule update --init --recursive - sudo pip3 install protobuf - - brew install qt@5 opus openssl@1.1 nasm sdl2 protobuf + - HOMEBREW_NO_AUTO_UPDATE=1 brew install qt@5 opus openssl@1.1 nasm sdl2 protobuf - scripts/build-ffmpeg.sh build_script: diff --git a/gui/CMakeLists.txt b/gui/CMakeLists.txt index 5e51b55..443150f 100644 --- a/gui/CMakeLists.txt +++ b/gui/CMakeLists.txt @@ -67,12 +67,12 @@ if(CHIAKI_ENABLE_CLI) endif() target_link_libraries(chiaki Qt5::Core Qt5::Widgets Qt5::Gui Qt5::Concurrent Qt5::Multimedia Qt5::OpenGL Qt5::Svg) +target_link_libraries(chiaki SDL2::SDL2) if(APPLE) target_link_libraries(chiaki Qt5::MacExtras) target_compile_definitions(chiaki PRIVATE CHIAKI_GUI_ENABLE_QT_MACEXTRAS) endif() if(CHIAKI_GUI_ENABLE_SDL_GAMECONTROLLER) - target_link_libraries(chiaki SDL2::SDL2) target_compile_definitions(chiaki PRIVATE CHIAKI_GUI_ENABLE_SDL_GAMECONTROLLER) endif() if(CHIAKI_ENABLE_SETSU) diff --git a/gui/include/controllermanager.h b/gui/include/controllermanager.h index 9e89fcc..eee0120 100644 --- a/gui/include/controllermanager.h +++ b/gui/include/controllermanager.h @@ -77,6 +77,7 @@ class Controller : public QObject int id; ChiakiOrientationTracker orientation_tracker; ChiakiControllerState state; + bool is_dualsense; #ifdef CHIAKI_GUI_ENABLE_SDL_GAMECONTROLLER QMap, uint8_t> touch_ids; @@ -91,9 +92,43 @@ class Controller : public QObject QString GetName(); ChiakiControllerState GetState(); void SetRumble(uint8_t left, uint8_t right); + void SetTriggerEffects(uint8_t type_left, const uint8_t *data_left, uint8_t type_right, const uint8_t *data_right); + bool IsDualSense(); signals: void StateChanged(); }; +/* PS5 trigger effect documentation: + https://controllers.fandom.com/wiki/Sony_DualSense#FFB_Trigger_Modes + + Taken from SDL2, licensed under the zlib license, + Copyright (C) 1997-2022 Sam Lantinga + https://github.com/libsdl-org/SDL/blob/release-2.24.1/test/testgamecontroller.c#L263-L289 +*/ +typedef struct +{ + Uint8 ucEnableBits1; /* 0 */ + Uint8 ucEnableBits2; /* 1 */ + Uint8 ucRumbleRight; /* 2 */ + Uint8 ucRumbleLeft; /* 3 */ + Uint8 ucHeadphoneVolume; /* 4 */ + Uint8 ucSpeakerVolume; /* 5 */ + Uint8 ucMicrophoneVolume; /* 6 */ + Uint8 ucAudioEnableBits; /* 7 */ + Uint8 ucMicLightMode; /* 8 */ + Uint8 ucAudioMuteBits; /* 9 */ + Uint8 rgucRightTriggerEffect[11]; /* 10 */ + Uint8 rgucLeftTriggerEffect[11]; /* 21 */ + Uint8 rgucUnknown1[6]; /* 32 */ + Uint8 ucLedFlags; /* 38 */ + Uint8 rgucUnknown2[2]; /* 39 */ + Uint8 ucLedAnim; /* 41 */ + Uint8 ucLedBrightness; /* 42 */ + Uint8 ucPadLights; /* 43 */ + Uint8 ucLedRed; /* 44 */ + Uint8 ucLedGreen; /* 45 */ + Uint8 ucLedBlue; /* 46 */ +} DS5EffectsState_t; + #endif // CHIAKI_CONTROLLERMANAGER_H diff --git a/gui/include/settings.h b/gui/include/settings.h index c2320b8..00f38ab 100644 --- a/gui/include/settings.h +++ b/gui/include/settings.h @@ -63,6 +63,9 @@ class Settings : public QObject void SetLogVerbose(bool enabled) { settings.setValue("settings/log_verbose", enabled); } uint32_t GetLogLevelMask(); + bool GetDualSenseEnabled() const { return settings.value("settings/dualsense_enabled", false).toBool(); } + void SetDualSenseEnabled(bool enabled) { settings.setValue("settings/dualsense_enabled", enabled); } + ChiakiVideoResolutionPreset GetResolution() const; void SetResolution(ChiakiVideoResolutionPreset resolution); diff --git a/gui/include/settingsdialog.h b/gui/include/settingsdialog.h index d564bef..00af648 100644 --- a/gui/include/settingsdialog.h +++ b/gui/include/settingsdialog.h @@ -20,6 +20,7 @@ class SettingsDialog : public QDialog QCheckBox *log_verbose_check_box; QComboBox *disconnect_action_combo_box; + QCheckBox *dualsense_check_box; QComboBox *resolution_combo_box; QComboBox *fps_combo_box; @@ -37,6 +38,7 @@ class SettingsDialog : public QDialog private slots: void LogVerboseChanged(); + void DualSenseChanged(); void DisconnectActionSelected(); void ResolutionSelected(); diff --git a/gui/include/streamsession.h b/gui/include/streamsession.h index e139f45..4b0ba01 100644 --- a/gui/include/streamsession.h +++ b/gui/include/streamsession.h @@ -56,6 +56,7 @@ struct StreamSessionConnectInfo bool fullscreen; TransformMode transform_mode; bool enable_keyboard; + bool enable_dualsense; StreamSessionConnectInfo( Settings *settings, @@ -101,17 +102,23 @@ class StreamSession : public QObject unsigned int audio_buffer_size; QAudioOutput *audio_output; QIODevice *audio_io; + SDL_AudioDeviceID haptics_output; + uint8_t *haptics_resampler_buf; QMap key_map; void PushAudioFrame(int16_t *buf, size_t samples_count); + void PushHapticsFrame(uint8_t *buf, size_t buf_size); #if CHIAKI_GUI_ENABLE_SETSU void HandleSetsuEvent(SetsuEvent *event); #endif private slots: void InitAudio(unsigned int channels, unsigned int rate); + void InitHaptics(); void Event(ChiakiEvent *event); + void DisconnectHaptics(); + void ConnectHaptics(); public: explicit StreamSession(const StreamSessionConnectInfo &connect_info, QObject *parent = nullptr); diff --git a/gui/src/controllermanager.cpp b/gui/src/controllermanager.cpp index b1f38e7..8cc501f 100644 --- a/gui/src/controllermanager.cpp +++ b/gui/src/controllermanager.cpp @@ -68,6 +68,12 @@ static QSet chiaki_motion_controller_guids({ "030000008f0e00001431000000000000", }); +static QSet> chiaki_dualsense_controller_ids({ + // in format (vendor id, product id) + QPair(0x054c, 0x0ce6), // DualSense controller + QPair(0x054c, 0x0df2), // DualSense Edge controller +}); + static ControllerManager *instance = nullptr; #define UPDATE_INTERVAL_MS 4 @@ -84,6 +90,15 @@ ControllerManager::ControllerManager(QObject *parent) { #ifdef CHIAKI_GUI_ENABLE_SDL_GAMECONTROLLER SDL_SetMainReady(); +#ifdef SDL_HINT_JOYSTICK_HIDAPI_PS4_RUMBLE + SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_PS4_RUMBLE, "1"); +#endif +#ifdef SDL_HINT_JOYSTICK_HIDAPI_PS5_RUMBLE + SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_PS5_RUMBLE, "1"); +#endif +#ifdef SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS + SDL_SetHint(SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS, "1"); +#endif if(SDL_Init(SDL_INIT_GAMECONTROLLER) < 0) { const char *err = SDL_GetError(); @@ -225,7 +240,8 @@ void ControllerManager::ControllerClosed(Controller *controller) open_controllers.remove(controller->GetDeviceID()); } -Controller::Controller(int device_id, ControllerManager *manager) : QObject(manager) +Controller::Controller(int device_id, ControllerManager *manager) + : QObject(manager), is_dualsense(false) { this->id = device_id; this->manager = manager; @@ -244,8 +260,10 @@ Controller::Controller(int device_id, ControllerManager *manager) : QObject(mana SDL_GameControllerSetSensorEnabled(controller, SDL_SENSOR_ACCEL, SDL_TRUE); if(SDL_GameControllerHasSensor(controller, SDL_SENSOR_GYRO)) SDL_GameControllerSetSensorEnabled(controller, SDL_SENSOR_GYRO, SDL_TRUE); - break; #endif + auto controller_id = QPair(SDL_GameControllerGetVendor(controller), SDL_GameControllerGetProduct(controller)); + is_dualsense = chiaki_dualsense_controller_ids.contains(controller_id); + break; } } #endif @@ -255,7 +273,12 @@ Controller::~Controller() { #ifdef CHIAKI_GUI_ENABLE_SDL_GAMECONTROLLER if(controller) + { + // Clear trigger effects, SDL doesn't do it automatically + const uint8_t clear_effect[10] = { 0 }; + this->SetTriggerEffects(0x05, clear_effect, 0x05, clear_effect); SDL_GameControllerClose(controller); + } #endif manager->ControllerClosed(this); } @@ -487,3 +510,28 @@ void Controller::SetRumble(uint8_t left, uint8_t right) SDL_GameControllerRumble(controller, (uint16_t)left << 8, (uint16_t)right << 8, 5000); #endif } + +void Controller::SetTriggerEffects(uint8_t type_left, const uint8_t *data_left, uint8_t type_right, const uint8_t *data_right) +{ +#if defined(CHIAKI_GUI_ENABLE_SDL_GAMECONTROLLER) && SDL_VERSION_ATLEAST(2, 0, 16) + if(!is_dualsense || !controller) + return; + DS5EffectsState_t state; + SDL_zero(state); + state.ucEnableBits1 |= (0x04 /* left trigger */ | 0x08 /* right trigger */); + state.rgucLeftTriggerEffect[0] = type_left; + SDL_memcpy(state.rgucLeftTriggerEffect + 1, data_left, 10); + state.rgucRightTriggerEffect[0] = type_right; + SDL_memcpy(state.rgucRightTriggerEffect + 1, data_right, 10); + SDL_GameControllerSendEffect(controller, &state, sizeof(state)); +#endif +} + +bool Controller::IsDualSense() +{ +#ifdef CHIAKI_GUI_ENABLE_SDL_GAMECONTROLLER + if(!controller) + return false; + return is_dualsense; +#endif +} diff --git a/gui/src/settingsdialog.cpp b/gui/src/settingsdialog.cpp index 3f8856f..1770932 100644 --- a/gui/src/settingsdialog.cpp +++ b/gui/src/settingsdialog.cpp @@ -69,6 +69,11 @@ SettingsDialog::SettingsDialog(Settings *settings, QWidget *parent) : QDialog(pa log_verbose_check_box->setChecked(settings->GetLogVerbose()); connect(log_verbose_check_box, &QCheckBox::stateChanged, this, &SettingsDialog::LogVerboseChanged); + dualsense_check_box = new QCheckBox(this); + general_layout->addRow(tr("Extended DualSense Support:\nEnable haptics and adaptive triggers\nfor attached DualSense controllers.\nThis is currently experimental."), dualsense_check_box); + dualsense_check_box->setChecked(settings->GetDualSenseEnabled()); + connect(dualsense_check_box, &QCheckBox::stateChanged, this, &SettingsDialog::DualSenseChanged); + auto log_directory_label = new QLineEdit(GetLogBaseDir(), this); log_directory_label->setReadOnly(true); general_layout->addRow(tr("Log Directory:"), log_directory_label); @@ -322,6 +327,11 @@ void SettingsDialog::LogVerboseChanged() settings->SetLogVerbose(log_verbose_check_box->isChecked()); } +void SettingsDialog::DualSenseChanged() +{ + settings->SetDualSenseEnabled(dualsense_check_box->isChecked()); +} + void SettingsDialog::FPSSelected() { settings->SetFPS((ChiakiVideoFPSPreset)fps_combo_box->currentData().toInt()); diff --git a/gui/src/streamsession.cpp b/gui/src/streamsession.cpp index f5b7ace..42c9959 100644 --- a/gui/src/streamsession.cpp +++ b/gui/src/streamsession.cpp @@ -14,6 +14,12 @@ #define SETSU_UPDATE_INTERVAL_MS 4 +#ifdef Q_OS_LINUX +#define DUALSENSE_AUDIO_DEVICE_NEEDLE "DualSense" +#else +#define DUALSENSE_AUDIO_DEVICE_NEEDLE "Wireless Controller" +#endif + StreamSessionConnectInfo::StreamSessionConnectInfo( Settings *settings, ChiakiTarget target, @@ -39,10 +45,12 @@ StreamSessionConnectInfo::StreamSessionConnectInfo( this->fullscreen = fullscreen; this->transform_mode = transform_mode; this->enable_keyboard = false; // TODO: from settings + this->enable_dualsense = settings->GetDualSenseEnabled(); } static void AudioSettingsCb(uint32_t channels, uint32_t rate, void *user); static void AudioFrameCb(int16_t *buf, size_t samples_count, void *user); +static void HapticsFrameCb(uint8_t *buf, size_t buf_size, void *user); static void EventCb(ChiakiEvent *event, void *user); #if CHIAKI_GUI_ENABLE_SETSU static void SessionSetsuCb(SetsuEvent *event, void *user); @@ -57,7 +65,9 @@ StreamSession::StreamSession(const StreamSessionConnectInfo &connect_info, QObje pi_decoder(nullptr), #endif audio_output(nullptr), - audio_io(nullptr) + audio_io(nullptr), + haptics_output(0), + haptics_resampler_buf(nullptr) { connected = false; ChiakiErrorCode err; @@ -116,6 +126,7 @@ StreamSession::StreamSession(const StreamSessionConnectInfo &connect_info, QObje chiaki_connect_info.video_profile = connect_info.video_profile; chiaki_connect_info.video_profile_auto_downgrade = true; chiaki_connect_info.enable_keyboard = false; + chiaki_connect_info.enable_dualsense = connect_info.enable_dualsense; #if CHIAKI_LIB_ENABLE_PI_DECODER if(connect_info.decoder == Decoder::Pi && chiaki_connect_info.video_profile.codec != CHIAKI_CODEC_H264) @@ -144,6 +155,14 @@ StreamSession::StreamSession(const StreamSessionConnectInfo &connect_info, QObje chiaki_opus_decoder_get_sink(&opus_decoder, &audio_sink); chiaki_session_set_audio_sink(&session, &audio_sink); + if(connect_info.enable_dualsense) + { + ChiakiAudioSink haptics_sink; + haptics_sink.user = this; + haptics_sink.frame_cb = HapticsFrameCb; + chiaki_session_set_haptics_sink(&session, &haptics_sink); + } + #if CHIAKI_LIB_ENABLE_PI_DECODER if(pi_decoder) chiaki_session_set_video_sample_cb(&session, chiaki_pi_decoder_video_sample_cb, pi_decoder); @@ -181,6 +200,10 @@ StreamSession::StreamSession(const StreamSessionConnectInfo &connect_info, QObje #endif key_map = connect_info.key_map; + if(connect_info.enable_dualsense) + { + InitHaptics(); + } UpdateGamepads(); } @@ -208,6 +231,16 @@ StreamSession::~StreamSession() chiaki_ffmpeg_decoder_fini(ffmpeg_decoder); delete ffmpeg_decoder; } + if(haptics_output > 0) + { + SDL_CloseAudioDevice(haptics_output); + haptics_output = 0; + } + if(haptics_resampler_buf) + { + free(haptics_resampler_buf); + haptics_resampler_buf = nullptr; + } } void StreamSession::Start() @@ -312,6 +345,8 @@ void StreamSession::UpdateGamepads() { CHIAKI_LOGI(log.GetChiakiLog(), "Controller %d disconnected", controller->GetDeviceID()); controllers.remove(controller_id); + if(controller->IsDualSense()) + DisconnectHaptics(); delete controller; } } @@ -330,6 +365,11 @@ void StreamSession::UpdateGamepads() CHIAKI_LOGI(log.GetChiakiLog(), "Controller %d opened: \"%s\"", controller_id, controller->GetName().toLocal8Bit().constData()); connect(controller, &Controller::StateChanged, this, &StreamSession::SendFeedbackState); controllers[controller_id] = controller; + if(controller->IsDualSense()) + { + // Connect haptics audio device with a delay to give the sound system time to set up + QTimer::singleShot(1000, this, &StreamSession::ConnectHaptics); + } } } @@ -388,6 +428,82 @@ void StreamSession::InitAudio(unsigned int channels, unsigned int rate) channels, rate, audio_output->bufferSize()); } +void StreamSession::InitHaptics() +{ + haptics_output = 0; + haptics_resampler_buf = nullptr; +#ifdef Q_OS_LINUX + // Haptics work most reliably with Pipewire, so try to use that if available + SDL_SetHint("SDL_AUDIODRIVER", "pipewire"); +#endif + + if(SDL_Init(SDL_INIT_AUDIO) < 0) + { + CHIAKI_LOGE(log.GetChiakiLog(), "Could not initialize SDL Audio for haptics output: %s", SDL_GetError()); + return; + } + +#ifdef Q_OS_LINUX + if(!strstr(SDL_GetCurrentAudioDriver(), "pipewire")) + { + CHIAKI_LOGW( + log.GetChiakiLog(), + "Haptics output is not using Pipewire, this may not work reliably. (was: '%s')", + SDL_GetCurrentAudioDriver()); + } +#endif + + SDL_AudioCVT cvt; + SDL_BuildAudioCVT(&cvt, AUDIO_S16LSB, 4, 3000, AUDIO_S16LSB, 4, 48000); + cvt.len = 240; // 10 16bit stereo samples + haptics_resampler_buf = (uint8_t*) calloc(cvt.len * cvt.len_mult, sizeof(uint8_t)); +} + +void StreamSession::DisconnectHaptics() +{ + if(this->haptics_output > 0) + { + SDL_CloseAudioDevice(haptics_output); + this->haptics_output = 0; + } +} + +void StreamSession::ConnectHaptics() +{ + if(this->haptics_output > 0) + { + CHIAKI_LOGW(this->log.GetChiakiLog(), "Haptics already connected to an attached DualSense controller, ignoring additional controllers."); + return; + } + + SDL_AudioSpec want, have; + SDL_zero(want); + want.freq = 48000; + want.format = AUDIO_S16LSB; + want.channels = 4; + want.samples = 480; // 10ms buffer + want.callback = NULL; + + const char *device_name = nullptr; + for(int i=0; i < SDL_GetNumAudioDevices(0); i++) + { + device_name = SDL_GetAudioDeviceName(i, 0); + if(!device_name || !strstr(device_name, DUALSENSE_AUDIO_DEVICE_NEEDLE)) + continue; + haptics_output = SDL_OpenAudioDevice(device_name, 0, &want, &have, 0); + if(haptics_output == 0) + { + CHIAKI_LOGE(log.GetChiakiLog(), "Could not open SDL Audio Device %s for haptics output: %s", device_name, SDL_GetError()); + continue; + } + SDL_PauseAudioDevice(haptics_output, 0); + CHIAKI_LOGI(log.GetChiakiLog(), "Haptics Audio Device '%s' opened with %d channels @ %d Hz, buffer size %u (driver=%s)", device_name, have.channels, have.freq, have.size, SDL_GetCurrentAudioDriver()); + return; + } + CHIAKI_LOGW(log.GetChiakiLog(), "DualSense features were enabled and a DualSense is connected, but could not find the DualSense audio device!"); + return; +} + void StreamSession::PushAudioFrame(int16_t *buf, size_t samples_count) { if(!audio_io) @@ -395,6 +511,35 @@ void StreamSession::PushAudioFrame(int16_t *buf, size_t samples_count) audio_io->write((const char *)buf, static_cast(samples_count * 2 * 2)); } +void StreamSession::PushHapticsFrame(uint8_t *buf, size_t buf_size) +{ + if(haptics_output == 0) + return; + SDL_AudioCVT cvt; + // Haptics samples are coming in at 3KHZ, but the DualSense expects 48KHZ + SDL_BuildAudioCVT(&cvt, AUDIO_S16LSB, 4, 3000, AUDIO_S16LSB, 4, 48000); + cvt.len = buf_size * 2; + cvt.buf = haptics_resampler_buf; + // Remix to 4 channels + for (int i=0; i < buf_size; i+=4) + { + SDL_memset(haptics_resampler_buf + i * 2, 0, 4); + SDL_memcpy(haptics_resampler_buf + (i * 2) + 4, buf + i, 4); + } + // Resample to 48kHZ + if(SDL_ConvertAudio(&cvt) != 0) + { + CHIAKI_LOGE(log.GetChiakiLog(), "Failed to resample haptics audio: %s", SDL_GetError()); + return; + } + + if(SDL_QueueAudio(haptics_output, cvt.buf, cvt.len_cvt) < 0) + { + CHIAKI_LOGE(log.GetChiakiLog(), "Failed to submit haptics audio to device: %s", SDL_GetError()); + return; + } +} + void StreamSession::Event(ChiakiEvent *event) { switch(event->type) @@ -418,6 +563,19 @@ void StreamSession::Event(ChiakiEvent *event) }); break; } + case CHIAKI_EVENT_TRIGGER_EFFECTS: { + uint8_t type_left = event->trigger_effects.type_left; + uint8_t data_left[10]; + memcpy(data_left, event->trigger_effects.left, 10); + uint8_t data_right[10]; + memcpy(data_right, event->trigger_effects.right, 10); + uint8_t type_right = event->trigger_effects.type_right; + QMetaObject::invokeMethod(this, [this, type_left, data_left, type_right, data_right]() { + for(auto controller : controllers) + controller->SetTriggerEffects(type_left, data_left, type_right, data_right); + }); + break; + } default: break; } @@ -544,6 +702,7 @@ class StreamSessionPrivate } static void PushAudioFrame(StreamSession *session, int16_t *buf, size_t samples_count) { session->PushAudioFrame(buf, samples_count); } + static void PushHapticsFrame(StreamSession *session, uint8_t *buf, size_t buf_size) { session->PushHapticsFrame(buf, buf_size); } static void Event(StreamSession *session, ChiakiEvent *event) { session->Event(event); } #if CHIAKI_GUI_ENABLE_SETSU static void HandleSetsuEvent(StreamSession *session, SetsuEvent *event) { session->HandleSetsuEvent(event); } @@ -563,6 +722,12 @@ static void AudioFrameCb(int16_t *buf, size_t samples_count, void *user) StreamSessionPrivate::PushAudioFrame(session, buf, samples_count); } +static void HapticsFrameCb(uint8_t *buf, size_t buf_size, void *user) +{ + auto session = reinterpret_cast(user); + StreamSessionPrivate::PushHapticsFrame(session, buf, buf_size); +} + static void EventCb(ChiakiEvent *event, void *user) { auto session = reinterpret_cast(user); diff --git a/scripts/appveyor-win.sh b/scripts/appveyor-win.sh index a7e63a0..7bbc0f3 100755 --- a/scripts/appveyor-win.sh +++ b/scripts/appveyor-win.sh @@ -30,14 +30,15 @@ ninja ninja install cd ../.. -wget https://mirror.firedaemon.com/OpenSSL/openssl-1.1.1q.zip && 7z x openssl-1.1.1q.zip +wget https://download.firedaemon.com/FireDaemon-OpenSSL/openssl-1.1.1s.zip && 7z x openssl-1.1.*.zip -wget https://www.libsdl.org/release/SDL2-devel-2.0.10-VC.zip && 7z x SDL2-devel-2.0.10-VC.zip -export SDL_ROOT="$BUILD_ROOT/SDL2-2.0.10" +wget https://www.libsdl.org/release/SDL2-devel-2.26.2-VC.zip && 7z x SDL2-devel-2.26.2-VC.zip +export SDL_ROOT="$BUILD_ROOT/SDL2-2.26.2" export SDL_ROOT=${SDL_ROOT//[\\]//} echo "set(SDL2_INCLUDE_DIRS \"$SDL_ROOT/include\") set(SDL2_LIBRARIES \"$SDL_ROOT/lib/x64/SDL2.lib\") -set(SDL2_LIBDIR \"$SDL_ROOT/lib/x64\")" > "$SDL_ROOT/SDL2Config.cmake" +set(SDL2_LIBDIR \"$SDL_ROOT/lib/x64\") +include($SDL_ROOT/cmake/sdl2-config-version.cmake)" > "$SDL_ROOT/SDL2Config.cmake" mkdir protoc && cd protoc wget https://github.com/protocolbuffers/protobuf/releases/download/v3.9.1/protoc-3.9.1-win64.zip && 7z x protoc-3.9.1-win64.zip @@ -55,6 +56,7 @@ echo "-- Configure" mkdir build && cd build + cmake \ -G Ninja \ -DCMAKE_C_COMPILER=cl \ diff --git a/scripts/build-sdl2.sh b/scripts/build-sdl2.sh index 725bbf4..716b486 100755 --- a/scripts/build-sdl2.sh +++ b/scripts/build-sdl2.sh @@ -22,14 +22,14 @@ mkdir -p build && cd build || exit 1 cmake \ -DCMAKE_INSTALL_PREFIX="$ROOT/sdl2-prefix" \ -DSDL_ATOMIC=OFF \ - -DSDL_AUDIO=OFF \ + -DSDL_AUDIO=ON \ -DSDL_CPUINFO=OFF \ -DSDL_EVENTS=ON \ -DSDL_FILE=OFF \ -DSDL_FILESYSTEM=OFF \ -DSDL_HAPTIC=ON \ -DSDL_JOYSTICK=ON \ - -DSDL_LOADSO=OFF \ + -DSDL_LOADSO=ON \ -DSDL_RENDER=OFF \ -DSDL_SHARED=ON \ -DSDL_STATIC=OFF \ diff --git a/scripts/kitware-archive-latest.asc b/scripts/kitware-archive-latest.asc index 6b3a357..2c95d3e 100644 --- a/scripts/kitware-archive-latest.asc +++ b/scripts/kitware-archive-latest.asc @@ -1,64 +1,64 @@ -----BEGIN PGP PUBLIC KEY BLOCK----- -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 -=cUKW +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 +=8BHh -----END PGP PUBLIC KEY BLOCK----- From e14083c87c10d734e0cdecd133d8a9e0d6bd00e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Sun, 5 Feb 2023 17:05:06 +0100 Subject: [PATCH 226/237] Update android target SDK to 33 and dependencies --- .builds/android.yml | 2 +- android/app/build.gradle | 29 +++++++++---------- android/app/src/main/AndroidManifest.xml | 4 ++- android/app/src/main/cpp/oboe | 2 +- .../com/metallic/chiaki/common/ManualHost.kt | 2 +- .../metallic/chiaki/common/RegisteredHost.kt | 2 +- .../chiaki/common/SerializedSettings.kt | 4 ++- .../metallic/chiaki/stream/StreamActivity.kt | 6 ++-- android/build.gradle | 4 +-- .../gradle/wrapper/gradle-wrapper.properties | 6 ++-- cmake/OpenSSLExternalProject.cmake | 4 +-- 11 files changed, 35 insertions(+), 30 deletions(-) diff --git a/.builds/android.yml b/.builds/android.yml index 5e4c259..20cd88b 100644 --- a/.builds/android.yml +++ b/.builds/android.yml @@ -22,7 +22,7 @@ tasks: sudo docker run \ -v /home/build:/home/build \ -u $(id -u):$(id -g) \ - thestr4ng3r/android:b2853cc \ + thestr4ng3r/android:90d826e \ /bin/bash -c "cd /home/build/chiaki/android && ./gradlew assembleRelease bundleRelease" cp chiaki/android/app/build/outputs/apk/release/app-release*.apk Chiaki.apk cp chiaki/android/app/build/outputs/bundle/release/app-release*.aab Chiaki.aab diff --git a/android/app/build.gradle b/android/app/build.gradle index 87b8761..99194ee 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -18,12 +18,11 @@ def chiakiVersion = "$chiakiVersionMajor.$chiakiVersionMinor.$chiakiVersionPatch println("Determined Chiaki Version: $chiakiVersion") android { - compileSdkVersion 30 - buildToolsVersion "30.0.2" + compileSdkVersion 33 defaultConfig { applicationId "com.metallic.chiaki" minSdkVersion 21 - targetSdkVersion 30 + targetSdkVersion 33 versionCode 11 versionName chiakiVersion externalNativeBuild { @@ -52,7 +51,7 @@ android { } externalNativeBuild { cmake { - version "3.10.2+" + version "3.22.1" path rootCMakeLists } } @@ -95,23 +94,23 @@ tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all { dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" - implementation 'androidx.appcompat:appcompat:1.2.0' - implementation 'androidx.core:core-ktx:1.3.2' - implementation 'androidx.constraintlayout:constraintlayout:2.0.4' - implementation 'androidx.recyclerview:recyclerview:1.1.0' - implementation 'androidx.preference:preference:1.1.1' - implementation 'com.google.android.material:material:1.2.1' + implementation 'androidx.appcompat:appcompat:1.6.0' + implementation 'androidx.core:core-ktx:1.9.0' + implementation 'androidx.constraintlayout:constraintlayout:2.1.4' + implementation 'androidx.recyclerview:recyclerview:1.2.1' + implementation 'androidx.preference:preference:1.2.0' + implementation 'com.google.android.material:material:1.8.0' implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0' - implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0' - implementation 'androidx.lifecycle:lifecycle-reactivestreams:2.2.0' + implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1' + implementation 'androidx.lifecycle:lifecycle-reactivestreams:2.5.1' implementation "io.reactivex.rxjava2:rxjava:2.2.20" implementation "io.reactivex.rxjava2:rxkotlin:2.4.0" implementation 'io.reactivex.rxjava2:rxandroid:2.1.1' - def room_version = "2.2.6" + def room_version = "2.5.0" implementation "androidx.room:room-runtime:$room_version" kapt "androidx.room:room-compiler:$room_version" implementation "androidx.room:room-ktx:$room_version" implementation "androidx.room:room-rxjava2:$room_version" - implementation "com.squareup.moshi:moshi:1.9.2" - kapt "com.squareup.moshi:moshi-kotlin-codegen:1.9.2" + implementation "com.squareup.moshi:moshi:1.14.0" + kapt "com.squareup.moshi:moshi-kotlin-codegen:1.14.0" } diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index fa14662..a0d28cb 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -6,6 +6,7 @@ + + android:configChanges="keyboard|keyboardHidden|orientation|screenSize" + android:exported="true"> diff --git a/android/app/src/main/cpp/oboe b/android/app/src/main/cpp/oboe index 0ab5b12..8740d0f 160000 --- a/android/app/src/main/cpp/oboe +++ b/android/app/src/main/cpp/oboe @@ -1 +1 @@ -Subproject commit 0ab5b12a5bc3630a3d6c83b20eed2a669ebf7a24 +Subproject commit 8740d0fc321a55489dbbf6067298201b7d2e106d diff --git a/android/app/src/main/java/com/metallic/chiaki/common/ManualHost.kt b/android/app/src/main/java/com/metallic/chiaki/common/ManualHost.kt index 5825000..7065610 100644 --- a/android/app/src/main/java/com/metallic/chiaki/common/ManualHost.kt +++ b/android/app/src/main/java/com/metallic/chiaki/common/ManualHost.kt @@ -3,7 +3,7 @@ package com.metallic.chiaki.common import androidx.room.* -import androidx.room.ForeignKey.SET_NULL +import androidx.room.ForeignKey.Companion.SET_NULL import io.reactivex.Completable import io.reactivex.Flowable import io.reactivex.Single diff --git a/android/app/src/main/java/com/metallic/chiaki/common/RegisteredHost.kt b/android/app/src/main/java/com/metallic/chiaki/common/RegisteredHost.kt index b96d88d..d920a8c 100644 --- a/android/app/src/main/java/com/metallic/chiaki/common/RegisteredHost.kt +++ b/android/app/src/main/java/com/metallic/chiaki/common/RegisteredHost.kt @@ -3,7 +3,7 @@ package com.metallic.chiaki.common import androidx.room.* -import androidx.room.ColumnInfo.BLOB +import androidx.room.ColumnInfo.Companion.BLOB import com.metallic.chiaki.lib.RegistHost import com.metallic.chiaki.lib.Target import io.reactivex.Completable diff --git a/android/app/src/main/java/com/metallic/chiaki/common/SerializedSettings.kt b/android/app/src/main/java/com/metallic/chiaki/common/SerializedSettings.kt index 8334dc5..74257ac 100644 --- a/android/app/src/main/java/com/metallic/chiaki/common/SerializedSettings.kt +++ b/android/app/src/main/java/com/metallic/chiaki/common/SerializedSettings.kt @@ -25,6 +25,8 @@ import io.reactivex.rxkotlin.addTo import io.reactivex.schedulers.Schedulers import okio.Buffer import okio.Okio +import okio.buffer +import okio.source import java.io.File import java.io.IOException @@ -164,7 +166,7 @@ fun importSettingsFromUri(activity: Activity, uri: Uri, disposable: CompositeDis try { val inputStream = activity.contentResolver.openInputStream(uri) ?: throw IOException() - val buffer = Okio.buffer(Okio.source(inputStream)) + val buffer = inputStream.source().buffer() val reader = JsonReader.of(buffer) val adapter = moshi().serializedSettingsAdapter() diff --git a/android/app/src/main/java/com/metallic/chiaki/stream/StreamActivity.kt b/android/app/src/main/java/com/metallic/chiaki/stream/StreamActivity.kt index b0248be..a332961 100644 --- a/android/app/src/main/java/com/metallic/chiaki/stream/StreamActivity.kt +++ b/android/app/src/main/java/com/metallic/chiaki/stream/StreamActivity.kt @@ -174,7 +174,7 @@ class StreamActivity : AppCompatActivity(), View.OnSystemUiVisibilityChangeListe .alpha(1.0f) .setListener(object: AnimatorListenerAdapter() { - override fun onAnimationEnd(animation: Animator?) + override fun onAnimationEnd(animation: Animator) { binding.overlay.alpha = 1.0f } @@ -189,7 +189,7 @@ class StreamActivity : AppCompatActivity(), View.OnSystemUiVisibilityChangeListe .alpha(0.0f) .setListener(object: AnimatorListenerAdapter() { - override fun onAnimationEnd(animation: Animator?) + override fun onAnimationEnd(animation: Animator) { binding.overlay.isGone = true } @@ -306,6 +306,8 @@ class StreamActivity : AppCompatActivity(), View.OnSystemUiVisibilityChangeListe dialog.show() } } + + else -> {} } } diff --git a/android/build.gradle b/android/build.gradle index 7ec4859..cdfc5d6 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -1,14 +1,14 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { - ext.kotlin_version = '1.4.21' + ext.kotlin_version = '1.8.0' repositories { google() jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:4.1.1' + classpath 'com.android.tools.build:gradle:7.4.1' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index c21407a..7a0d628 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Fri Jan 15 11:37:05 CET 2021 +#Sun Feb 05 16:25:19 CET 2023 distributionBase=GRADLE_USER_HOME +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-bin.zip distributionPath=wrapper/dists -zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-bin.zip +zipStoreBase=GRADLE_USER_HOME diff --git a/cmake/OpenSSLExternalProject.cmake b/cmake/OpenSSLExternalProject.cmake index 0538d48..1e1370d 100644 --- a/cmake/OpenSSLExternalProject.cmake +++ b/cmake/OpenSSLExternalProject.cmake @@ -33,8 +33,8 @@ endif() find_program(MAKE_EXE NAMES gmake make) ExternalProject_Add(OpenSSL-ExternalProject - URL https://www.openssl.org/source/openssl-1.1.1d.tar.gz - URL_HASH SHA256=1e3a91bc1f9dfce01af26026f856e064eab4c8ee0a8f457b5ae30b40b8b711f2 + URL https://www.openssl.org/source/openssl-1.1.1s.tar.gz + URL_HASH SHA256=c5ac01e760ee6ff0dab61d6b2bbd30146724d063eb322180c6f18a6f74e4b6aa INSTALL_DIR "${OPENSSL_INSTALL_DIR}" CONFIGURE_COMMAND ${CMAKE_COMMAND} -E env ${OPENSSL_BUILD_ENV} "/Configure" "--prefix=" no-shared ${OPENSSL_CONFIG_EXTRA_ARGS} "${OPENSSL_OS_COMPILER}" From 582ec7aa5459b2916a430d6b761ca8c76da8e639 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Wed, 8 Feb 2023 14:08:17 +0100 Subject: [PATCH 227/237] Allow specifying command in switch podman script --- scripts/switch/run-podman-build-chiaki.sh | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/scripts/switch/run-podman-build-chiaki.sh b/scripts/switch/run-podman-build-chiaki.sh index f9e05c0..0e8dede 100755 --- a/scripts/switch/run-podman-build-chiaki.sh +++ b/scripts/switch/run-podman-build-chiaki.sh @@ -7,5 +7,4 @@ podman run --rm \ -w "/build/chiaki" \ -it \ thestr4ng3r/chiaki-build-switch:v2 \ - /bin/bash -c "scripts/switch/build.sh" - + ${1:-/bin/bash -c "scripts/switch/build.sh"} From 6096de8c13881062bc40cf1801afdf3e3d6ce1d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Wed, 8 Feb 2023 14:08:45 +0100 Subject: [PATCH 228/237] Do not handle server shutdown as error When the console is powered off or put into sleep mode during streaming, the remote will disconnect and report the string "Server shutting down". This is expected by the user and thus should not be shown as an error message. --- android/app/src/main/cpp/chiaki-jni.c | 4 +- .../java/com/metallic/chiaki/lib/Chiaki.kt | 7 +-- .../metallic/chiaki/stream/StreamActivity.kt | 47 ++++++++++--------- gui/src/streamwindow.cpp | 2 +- lib/include/chiaki/session.h | 8 +++- lib/src/session.c | 7 ++- 6 files changed, 44 insertions(+), 31 deletions(-) diff --git a/android/app/src/main/cpp/chiaki-jni.c b/android/app/src/main/cpp/chiaki-jni.c index d0b374a..9b14b60 100644 --- a/android/app/src/main/cpp/chiaki-jni.c +++ b/android/app/src/main/cpp/chiaki-jni.c @@ -110,9 +110,9 @@ JNIEXPORT jstring JNICALL JNI_FCN(quitReasonToString)(JNIEnv *env, jobject obj, return E->NewStringUTF(env, chiaki_quit_reason_string((ChiakiQuitReason)value)); } -JNIEXPORT jboolean JNICALL JNI_FCN(quitReasonIsStopped)(JNIEnv *env, jobject obj, jint value) +JNIEXPORT jboolean JNICALL JNI_FCN(quitReasonIsError)(JNIEnv *env, jobject obj, jint value) { - return value == CHIAKI_QUIT_REASON_STOPPED; + return chiaki_quit_reason_is_error(value); } JNIEXPORT jobject JNICALL JNI_FCN(videoProfilePreset)(JNIEnv *env, jobject obj, jint resolution_preset, jint fps_preset, jobject codec) diff --git a/android/app/src/main/java/com/metallic/chiaki/lib/Chiaki.kt b/android/app/src/main/java/com/metallic/chiaki/lib/Chiaki.kt index 2c474f0..bb0b0a6 100644 --- a/android/app/src/main/java/com/metallic/chiaki/lib/Chiaki.kt +++ b/android/app/src/main/java/com/metallic/chiaki/lib/Chiaki.kt @@ -83,7 +83,7 @@ private class ChiakiNative } @JvmStatic external fun errorCodeToString(value: Int): String @JvmStatic external fun quitReasonToString(value: Int): String - @JvmStatic external fun quitReasonIsStopped(value: Int): Boolean + @JvmStatic external fun quitReasonIsError(value: Int): Boolean @JvmStatic external fun videoProfilePreset(resolutionPreset: Int, fpsPreset: Int, codec: Codec): ConnectVideoProfile @JvmStatic external fun sessionCreate(result: CreateResult, connectInfo: ConnectInfo, logFile: String?, logVerbose: Boolean, javaSession: Session) @JvmStatic external fun sessionFree(ptr: Long) @@ -309,10 +309,7 @@ class QuitReason(val value: Int) { override fun toString() = ChiakiNative.quitReasonToString(value) - /** - * whether the reason is CHIAKI_QUIT_REASON_STOPPED - */ - val isStopped = ChiakiNative.quitReasonIsStopped(value) + val isError = ChiakiNative.quitReasonIsError(value) } sealed class Event diff --git a/android/app/src/main/java/com/metallic/chiaki/stream/StreamActivity.kt b/android/app/src/main/java/com/metallic/chiaki/stream/StreamActivity.kt index a332961..bd77e0c 100644 --- a/android/app/src/main/java/com/metallic/chiaki/stream/StreamActivity.kt +++ b/android/app/src/main/java/com/metallic/chiaki/stream/StreamActivity.kt @@ -230,28 +230,33 @@ class StreamActivity : AppCompatActivity(), View.OnSystemUiVisibilityChangeListe { is StreamStateQuit -> { - if(!state.reason.isStopped && dialogContents != StreamQuitDialog) + if(dialogContents != StreamQuitDialog) { - dialog?.dismiss() - val reasonStr = state.reasonString - val dialog = MaterialAlertDialogBuilder(this) - .setMessage(getString(R.string.alert_message_session_quit, state.reason.toString()) - + (if(reasonStr != null) "\n$reasonStr" else "")) - .setPositiveButton(R.string.action_reconnect) { _, _ -> - dialog = null - reconnect() - } - .setOnCancelListener { - dialog = null - finish() - } - .setNegativeButton(R.string.action_quit_session) { _, _ -> - dialog = null - finish() - } - .create() - dialogContents = StreamQuitDialog - dialog.show() + if(state.reason.isError) + { + dialog?.dismiss() + val reasonStr = state.reasonString + val dialog = MaterialAlertDialogBuilder(this) + .setMessage(getString(R.string.alert_message_session_quit, state.reason.toString()) + + (if(reasonStr != null) "\n$reasonStr" else "")) + .setPositiveButton(R.string.action_reconnect) { _, _ -> + dialog = null + reconnect() + } + .setOnCancelListener { + dialog = null + finish() + } + .setNegativeButton(R.string.action_quit_session) { _, _ -> + dialog = null + finish() + } + .create() + dialogContents = StreamQuitDialog + dialog.show() + } + else + finish() } } diff --git a/gui/src/streamwindow.cpp b/gui/src/streamwindow.cpp index d1c0b95..a2337d7 100644 --- a/gui/src/streamwindow.cpp +++ b/gui/src/streamwindow.cpp @@ -201,7 +201,7 @@ void StreamWindow::closeEvent(QCloseEvent *event) void StreamWindow::SessionQuit(ChiakiQuitReason reason, const QString &reason_str) { - if(reason != CHIAKI_QUIT_REASON_STOPPED) + if(chiaki_quit_reason_is_error(reason)) { QString m = tr("Chiaki Session has quit") + ":\n" + chiaki_quit_reason_string(reason); if(!reason_str.isEmpty()) diff --git a/lib/include/chiaki/session.h b/lib/include/chiaki/session.h index 4f9f932..5c355ad 100644 --- a/lib/include/chiaki/session.h +++ b/lib/include/chiaki/session.h @@ -94,11 +94,17 @@ typedef enum { CHIAKI_QUIT_REASON_CTRL_CONNECT_FAILED, CHIAKI_QUIT_REASON_CTRL_CONNECTION_REFUSED, CHIAKI_QUIT_REASON_STREAM_CONNECTION_UNKNOWN, - CHIAKI_QUIT_REASON_STREAM_CONNECTION_REMOTE_DISCONNECTED + CHIAKI_QUIT_REASON_STREAM_CONNECTION_REMOTE_DISCONNECTED, + CHIAKI_QUIT_REASON_STREAM_CONNECTION_REMOTE_SHUTDOWN, // like REMOTE_DISCONNECTED, but because the server shut down } ChiakiQuitReason; CHIAKI_EXPORT const char *chiaki_quit_reason_string(ChiakiQuitReason reason); +static inline bool chiaki_quit_reason_is_error(ChiakiQuitReason reason) +{ + return reason != CHIAKI_QUIT_REASON_STOPPED && reason != CHIAKI_QUIT_REASON_STREAM_CONNECTION_REMOTE_SHUTDOWN; +} + typedef struct chiaki_quit_event_t { ChiakiQuitReason reason; diff --git a/lib/src/session.c b/lib/src/session.c index ea8e09d..aa56e19 100644 --- a/lib/src/session.c +++ b/lib/src/session.c @@ -158,6 +158,8 @@ CHIAKI_EXPORT const char *chiaki_quit_reason_string(ChiakiQuitReason reason) return "Unknown Error in Stream Connection"; case CHIAKI_QUIT_REASON_STREAM_CONNECTION_REMOTE_DISCONNECTED: return "Remote has disconnected from Stream Connection"; + case CHIAKI_QUIT_REASON_STREAM_CONNECTION_REMOTE_SHUTDOWN: + return "Remote has disconnected from Stream Connection the because Server shut down"; case CHIAKI_QUIT_REASON_NONE: default: return "Unknown"; @@ -505,7 +507,10 @@ ctrl_failed: if(err == CHIAKI_ERR_DISCONNECTED) { CHIAKI_LOGE(session->log, "Remote disconnected from StreamConnection"); - session->quit_reason = CHIAKI_QUIT_REASON_STREAM_CONNECTION_REMOTE_DISCONNECTED; + if(!strcmp(session->stream_connection.remote_disconnect_reason, "Server shutting down")) + session->quit_reason = CHIAKI_QUIT_REASON_STREAM_CONNECTION_REMOTE_SHUTDOWN; + else + session->quit_reason = CHIAKI_QUIT_REASON_STREAM_CONNECTION_REMOTE_DISCONNECTED; session->quit_reason_str = strdup(session->stream_connection.remote_disconnect_reason); } else if(err != CHIAKI_ERR_SUCCESS && err != CHIAKI_ERR_CANCELED) From bcdd0dd7fd9fb6b83f830fafe3337ff14f200c14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Sat, 19 Aug 2023 17:00:54 +0200 Subject: [PATCH 229/237] Update CI images --- .builds/common.yml | 2 +- .builds/openbsd.yml | 2 +- scripts/build-appimage.sh | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.builds/common.yml b/.builds/common.yml index 0bce7cb..b692b08 100644 --- a/.builds/common.yml +++ b/.builds/common.yml @@ -1,5 +1,5 @@ -image: alpine/edge # on edge for https://gitlab.alpinelinux.org/alpine/aports/-/issues/13287 +image: alpine/latest sources: - https://git.sr.ht/~thestr4ng3r/chiaki diff --git a/.builds/openbsd.yml b/.builds/openbsd.yml index 2df5353..2f513c0 100644 --- a/.builds/openbsd.yml +++ b/.builds/openbsd.yml @@ -1,5 +1,5 @@ -image: openbsd/7.0 +image: openbsd/latest sources: - https://git.sr.ht/~thestr4ng3r/chiaki diff --git a/scripts/build-appimage.sh b/scripts/build-appimage.sh index 4fbd588..ab025af 100755 --- a/scripts/build-appimage.sh +++ b/scripts/build-appimage.sh @@ -48,4 +48,4 @@ export LD_LIBRARY_PATH="`pwd`/sdl2-prefix/lib:$LD_LIBRARY_PATH" export EXTRA_QT_PLUGINS=opengl ./linuxdeploy-x86_64.AppImage --appdir="${appdir}" -e "${appdir}/usr/bin/chiaki" -d "${appdir}/usr/share/applications/chiaki.desktop" --plugin qt --output appimage -mv Chiaki-*-x86_64.AppImage Chiaki.AppImage +mv Chiaki*-x86_64.AppImage Chiaki.AppImage From 666238ba9fbce9cc79c9b03f1370907d44f838c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Sun, 20 Aug 2023 11:12:11 +0200 Subject: [PATCH 230/237] Bump version to 2.2.0 --- CMakeLists.txt | 4 ++-- android/app/build.gradle | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 3af7f5c..64caaee 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -32,8 +32,8 @@ tri_option(CHIAKI_USE_SYSTEM_JERASURE "Use system-provided jerasure instead of s tri_option(CHIAKI_USE_SYSTEM_NANOPB "Use system-provided nanopb instead of submodule" AUTO) set(CHIAKI_VERSION_MAJOR 2) -set(CHIAKI_VERSION_MINOR 1) -set(CHIAKI_VERSION_PATCH 1) +set(CHIAKI_VERSION_MINOR 2) +set(CHIAKI_VERSION_PATCH 0) set(CHIAKI_VERSION ${CHIAKI_VERSION_MAJOR}.${CHIAKI_VERSION_MINOR}.${CHIAKI_VERSION_PATCH}) set(CPACK_PACKAGE_NAME "chiaki") diff --git a/android/app/build.gradle b/android/app/build.gradle index 99194ee..21a89f3 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -23,7 +23,7 @@ android { applicationId "com.metallic.chiaki" minSdkVersion 21 targetSdkVersion 33 - versionCode 11 + versionCode 12 versionName chiakiVersion externalNativeBuild { cmake { From d4a0603bf20a007d57aef2e63a384f0eb4041759 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Sun, 20 Aug 2023 11:17:05 +0200 Subject: [PATCH 231/237] Fix switch host_addr regex for more arbitrary strings --- switch/include/settings.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/switch/include/settings.h b/switch/include/settings.h index fbf43b8..72fc491 100644 --- a/switch/include/settings.h +++ b/switch/include/settings.h @@ -49,7 +49,7 @@ class Settings // the goal is to read/write inernal flat configuration file const std::map re_map = { {HOST_NAME, std::regex("^\\[\\s*(.+)\\s*\\]")}, - {HOST_ADDR, std::regex("^\\s*host_(?:ip|addr)\\s*=\\s*\"?((\\d+\\.\\d+\\.\\d+\\.\\d+)|([A-Za-z0-9-]{1,255}))\"?")}, + {HOST_ADDR, std::regex("^\\s*host_(?:ip|addr)\\s*=\\s*\"?([^\"]*)\"?")}, {PSN_ONLINE_ID, std::regex("^\\s*psn_online_id\\s*=\\s*\"?([\\w_-]+)\"?")}, {PSN_ACCOUNT_ID, std::regex("^\\s*psn_account_id\\s*=\\s*\"?([\\w/=+]+)\"?")}, {RP_KEY, std::regex("^\\s*rp_key\\s*=\\s*\"?([\\w/=+]+)\"?")}, From 89368f63c99d67cde8868c0269b66a1b0c507397 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Sun, 20 Aug 2023 11:21:25 +0200 Subject: [PATCH 232/237] Add script for local macOS distribution --- .gitignore | 2 ++ scripts/macos-dist-local.sh | 18 ++++++++++++++++++ 2 files changed, 20 insertions(+) create mode 100755 scripts/macos-dist-local.sh diff --git a/.gitignore b/.gitignore index 0b9eccd..f30ef42 100644 --- a/.gitignore +++ b/.gitignore @@ -29,3 +29,5 @@ compile_commands.json chiaki.conf /appimage .cache/ +/*.app +/*.dmg diff --git a/scripts/macos-dist-local.sh b/scripts/macos-dist-local.sh new file mode 100755 index 0000000..d2ac740 --- /dev/null +++ b/scripts/macos-dist-local.sh @@ -0,0 +1,18 @@ +#!/bin/bash + +# Build Chiaki for macOS distribution using dependencies from MacPorts and custom ffmpeg + +set -xe +cd $(dirname "${BASH_SOURCE[0]}")/.. +scripts/build-ffmpeg.sh +export CMAKE_PREFIX_PATH="`pwd`/ffmpeg-prefix" +scripts/build-common.sh +cp -a build/gui/chiaki.app Chiaki.app +/opt/local/libexec/qt5/bin/macdeployqt Chiaki.app + +# Remove all LC_RPATH load commands that have absolute paths of the build machine +RPATHS=$(otool -l Chiaki.app/Contents/MacOS/chiaki | grep -A 2 LC_RPATH | grep 'path /' | awk '{print $2}') +for p in ${RPATHS}; do install_name_tool -delete_rpath "$p" Chiaki.app/Contents/MacOS/chiaki; done + +# This may warn because we already ran macdeployqt above +/opt/local/libexec/qt5/bin/macdeployqt Chiaki.app -dmg From 8911a44766570c0d182c0e59f5eee5cfca548ce9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Sun, 20 Aug 2023 12:53:52 +0200 Subject: [PATCH 233/237] Remove Play Store from README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5be248c..49ec331 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ Builds are provided for Linux, Android, macOS, Nintendo Switch and Windows. You can download them [here](https://git.sr.ht/~thestr4ng3r/chiaki/refs). * **Linux**: The provided file is an [AppImage](https://appimage.org/). Simply make it executable (`chmod +x .AppImage`) and run it. -* **Android**: Install from [Google Play](https://play.google.com/store/apps/details?id=com.metallic.chiaki), [F-Droid](https://f-droid.org/packages/com.metallic.chiaki/) or download the APK from Sourcehut. +* **Android**: Install from [F-Droid](https://f-droid.org/packages/com.metallic.chiaki/) or download the APK from Sourcehut. * **macOS**: Drag the application from the `.dmg` into your Applications folder. * **Windows**: Extract the `.zip` file and execute `chiaki.exe`. * **Switch**: Download the `.nro` file and copy it into the `switch/` directory on your SD card. From 94fcdc3c6109cdcfbcf5524e5ab7838b230822b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Wed, 31 Jul 2024 15:52:37 +0200 Subject: [PATCH 234/237] Add reference to chiaki-ng --- README.md | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 49ec331..1a94b66 100644 --- a/README.md +++ b/README.md @@ -10,12 +10,17 @@ Chiaki is a Free and Open Source Software Client for PlayStation 4 and PlayStation 5 Remote Play for Linux, FreeBSD, OpenBSD, NetBSD, Android, macOS, Windows, Nintendo Switch and potentially even more platforms. -![Screenshot](assets/screenshot.png) - -## Project Status +## Project Status and Contributing As all relevant features are implemented, this project is considered to be finished and in maintenance mode only. -No major updates are planned and contributions are only accepted in special cases. +No major updates are planned and contributions are only accepted in special cases such as security issues. +The objective is to keep a stable base and not break existing support for less mainstream platforms such as BSDs. + +**For a more active, fast moving and community-oriented project, refer +to [chiaki-ng](https://streetpea.github.io/chiaki-ng/) ("next generation"). +If you would like to contribute, this will likely also be the best place to do so.** + +![Screenshot](assets/screenshot.png) ## Installing From 4eb90a7a658c93bca3b681b9d8e4282f21258b4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Mon, 9 Jun 2025 12:20:27 +0200 Subject: [PATCH 235/237] Refresh switch to build again --- scripts/switch/dev-container.sh | 10 ++++++++++ scripts/switch/run-podman-build-chiaki.sh | 2 +- switch/CMakeLists.txt | 15 ++++++--------- switch/include/io.h | 2 +- switch/include/settings.h | 1 - switch/src/main.cpp | 2 -- switch/src/settings.cpp | 12 +++--------- 7 files changed, 21 insertions(+), 23 deletions(-) create mode 100755 scripts/switch/dev-container.sh diff --git a/scripts/switch/dev-container.sh b/scripts/switch/dev-container.sh new file mode 100755 index 0000000..4cfedb3 --- /dev/null +++ b/scripts/switch/dev-container.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +cd "`dirname $(readlink -f ${0})`/../.." + +podman run --rm \ + -v "`pwd`:/build/chiaki" \ + -w "/build/chiaki" \ + -it \ + quay.io/thestr4ng3r/chiaki-build-switch:v3 \ + /bin/bash diff --git a/scripts/switch/run-podman-build-chiaki.sh b/scripts/switch/run-podman-build-chiaki.sh index 0e8dede..513cdc8 100755 --- a/scripts/switch/run-podman-build-chiaki.sh +++ b/scripts/switch/run-podman-build-chiaki.sh @@ -6,5 +6,5 @@ podman run --rm \ -v "`pwd`:/build/chiaki" \ -w "/build/chiaki" \ -it \ - thestr4ng3r/chiaki-build-switch:v2 \ + quay.io/thestr4ng3r/chiaki-build-switch:v3 \ ${1:-/bin/bash -c "scripts/switch/build.sh"} diff --git a/switch/CMakeLists.txt b/switch/CMakeLists.txt index 4916a2e..3d5639a 100644 --- a/switch/CMakeLists.txt +++ b/switch/CMakeLists.txt @@ -61,17 +61,16 @@ target_include_directories(borealis PUBLIC find_package(glfw3 REQUIRED) find_library(EGL EGL) -find_library(GLAPI glapi) -find_library(DRM_NOUVEAU drm_nouveau) -target_link_libraries(borealis +target_link_libraries(borealis PUBLIC glfw - ${EGL} - ${GLAPI} - ${DRM_NOUVEAU}) + ${EGL}) if(CHIAKI_IS_SWITCH) target_compile_definitions(borealis PUBLIC BOREALIS_RESOURCES="romfs:/") + find_library(GLAPI glapi) + find_library(DRM_NOUVEAU drm_nouveau) + target_link_libraries(borealis PUBLIC ${GLAPI} ${DRM_NOUVEAU}) else() target_compile_definitions(borealis PUBLIC BOREALIS_RESOURCES="./switch/res/") @@ -114,9 +113,7 @@ target_link_libraries(chiaki-borealis if(CHIAKI_IS_SWITCH) # libnx is forced by the switch toolchain find_library(Z z) - find_library(GLAPI glapi) # TODO: make it transitive from borealis - find_library(DRM_NOUVEAU drm_nouveau) # TODO: make it transitive from borealis - target_link_libraries(chiaki-borealis ${Z} ${GLAPI} ${DRM_NOUVEAU}) + target_link_libraries(chiaki-borealis ${Z} ${GLAPI}) endif() install(TARGETS chiaki-borealis diff --git a/switch/include/io.h b/switch/include/io.h index 3c1031d..3bb6fd1 100644 --- a/switch/include/io.h +++ b/switch/include/io.h @@ -67,7 +67,7 @@ class IO // default nintendo switch res int screen_width = 1280; int screen_height = 720; - AVCodec *codec; + const AVCodec *codec; AVCodecContext *codec_context; AVFrame *frame; SDL_AudioDeviceID sdl_audio_device_id = 0; diff --git a/switch/include/settings.h b/switch/include/settings.h index 72fc491..6bcb445 100644 --- a/switch/include/settings.h +++ b/switch/include/settings.h @@ -61,7 +61,6 @@ class Settings }; ConfigurationItem ParseLine(std::string * line, std::string * value); - size_t GetB64encodeSize(size_t); public: // singleton configuration diff --git a/switch/src/main.cpp b/switch/src/main.cpp index 6b7c95e..1dd33de 100644 --- a/switch/src/main.cpp +++ b/switch/src/main.cpp @@ -28,8 +28,6 @@ bool appletMainLoop() // use a custom nintendo switch socket config // chiaki requiers many threads with udp/tcp sockets static const SocketInitConfig g_chiakiSocketInitConfig = { - .bsdsockets_version = 1, - .tcp_tx_buf_size = 0x8000, .tcp_rx_buf_size = 0x10000, .tcp_tx_buf_max_size = 0x40000, diff --git a/switch/src/settings.cpp b/switch/src/settings.cpp index fb39861..7d3300a 100644 --- a/switch/src/settings.cpp +++ b/switch/src/settings.cpp @@ -29,11 +29,7 @@ Settings::ConfigurationItem Settings::ParseLine(std::string *line, std::string * return UNKNOWN; } -size_t Settings::GetB64encodeSize(size_t in) -{ - // calculate base64 buffer size after encode - return ((4 * in / 3) + 3) & ~3; -} +#define B64_ENCODED_SIZE(in) (((4 * in / 3) + 3) & ~3) Settings *Settings::instance = nullptr; @@ -458,8 +454,7 @@ std::string Settings::GetHostRPKey(Host *host) { if(host->rp_key_data || host->registered) { - size_t rp_key_b64_sz = this->GetB64encodeSize(0x10); - char rp_key_b64[rp_key_b64_sz + 1] = { 0 }; + char rp_key_b64[B64_ENCODED_SIZE(0x10) + 1] = { 0 }; ChiakiErrorCode err; err = chiaki_base64_encode( host->rp_key, 0x10, @@ -502,8 +497,7 @@ std::string Settings::GetHostRPRegistKey(Host *host) { if(host->rp_key_data || host->registered) { - size_t rp_regist_key_b64_sz = this->GetB64encodeSize(CHIAKI_SESSION_AUTH_SIZE); - char rp_regist_key_b64[rp_regist_key_b64_sz + 1] = { 0 }; + char rp_regist_key_b64[B64_ENCODED_SIZE(CHIAKI_SESSION_AUTH_SIZE) + 1] = { 0 }; ChiakiErrorCode err; err = chiaki_base64_encode( (uint8_t *)host->rp_regist_key, CHIAKI_SESSION_AUTH_SIZE, From bb5a79f2349a96e390c2089d3ce9a053705f0b2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Sun, 22 Jun 2025 11:57:41 +0200 Subject: [PATCH 236/237] Update dependencies in BSDs CI --- .builds/freebsd.yml | 5 +++-- .builds/openbsd.yml | 1 + 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.builds/freebsd.yml b/.builds/freebsd.yml index c62a695..72109b5 100644 --- a/.builds/freebsd.yml +++ b/.builds/freebsd.yml @@ -1,5 +1,5 @@ -image: freebsd/13.x +image: freebsd/14.x sources: - https://git.sr.ht/~thestr4ng3r/chiaki @@ -7,7 +7,8 @@ sources: packages: - cmake - protobuf - - py39-protobuf + - py311-setuptools # should not be needed with nanopb >= 0.4.9 + - py311-protobuf - opus - qt5-core - qt5-qmake diff --git a/.builds/openbsd.yml b/.builds/openbsd.yml index 2f513c0..e1595ac 100644 --- a/.builds/openbsd.yml +++ b/.builds/openbsd.yml @@ -7,6 +7,7 @@ sources: packages: - cmake - protobuf + - py3-setuptools # should not be needed with nanopb >= 0.4.9 - py3-protobuf - opus - qtbase From a1fd41868588db0d90dc5ee6c2eac2bc46a62408 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Wed, 25 Jun 2025 16:04:55 +0200 Subject: [PATCH 237/237] Update Ubuntu for AppImage to 24.04 --- scripts/Dockerfile.bionic | 16 ------- scripts/Dockerfile.noble | 13 ++++++ scripts/build-appimage.sh | 7 +-- scripts/build-ffmpeg.sh | 2 +- scripts/fetch-protoc.sh | 13 ------ scripts/kitware-archive-latest.asc | 64 ---------------------------- scripts/run-podman-build-appimage.sh | 5 ++- 7 files changed, 18 insertions(+), 102 deletions(-) delete mode 100644 scripts/Dockerfile.bionic create mode 100644 scripts/Dockerfile.noble delete mode 100755 scripts/fetch-protoc.sh delete mode 100644 scripts/kitware-archive-latest.asc diff --git a/scripts/Dockerfile.bionic b/scripts/Dockerfile.bionic deleted file mode 100644 index 2548a2d..0000000 --- a/scripts/Dockerfile.bionic +++ /dev/null @@ -1,16 +0,0 @@ - -FROM ubuntu:bionic - -RUN apt-get update -RUN apt-get install -y software-properties-common gpg wget -RUN add-apt-repository ppa:beineri/opt-qt-5.12.10-bionic -COPY kitware-archive-latest.asc /kitware-archive-latest.asc -RUN cat /kitware-archive-latest.asc | gpg --dearmor > /usr/share/keyrings/kitware-archive-keyring.gpg -RUN echo 'deb [signed-by=/usr/share/keyrings/kitware-archive-keyring.gpg] https://apt.kitware.com/ubuntu/ bionic main' > /etc/apt/sources.list.d/kitware.list -RUN apt-get update -RUN apt-get -y install git g++ cmake ninja-build curl pkg-config unzip python3-pip \ - libssl-dev libopus-dev qt512base qt512multimedia qt512svg \ - libgl1-mesa-dev nasm libudev-dev libva-dev fuse libevdev-dev libudev-dev - -CMD [] - diff --git a/scripts/Dockerfile.noble b/scripts/Dockerfile.noble new file mode 100644 index 0000000..abc33c3 --- /dev/null +++ b/scripts/Dockerfile.noble @@ -0,0 +1,13 @@ + +FROM ubuntu:noble + +RUN apt-get update +# Hint: python3-setuptools should not be needed with nanopb >= 0.4.9 +RUN apt-get -y install git g++ cmake ninja-build curl pkg-config unzip \ + python3-protobuf protobuf-compiler \ + python3-setuptools \ + libssl-dev libopus-dev qtbase5-dev qtmultimedia5-dev libqt5multimedia5-plugins libqt5svg5-dev \ + libgl1-mesa-dev nasm libudev-dev libva-dev fuse libevdev-dev libudev-dev file + +CMD [] + diff --git a/scripts/build-appimage.sh b/scripts/build-appimage.sh index ab025af..05c0428 100755 --- a/scripts/build-appimage.sh +++ b/scripts/build-appimage.sh @@ -7,8 +7,6 @@ appdir=${1:-`pwd`/appimage/appdir} mkdir appimage -pip3 install --user protobuf==3.19.5 # need support for python 3.6 for running on bionic -scripts/fetch-protoc.sh appimage export PATH="`pwd`/appimage/protoc/bin:$PATH" scripts/build-ffmpeg.sh appimage scripts/build-sdl2.sh appimage @@ -18,7 +16,7 @@ cd build_appimage cmake \ -GNinja \ -DCMAKE_BUILD_TYPE=Release \ - "-DCMAKE_PREFIX_PATH=`pwd`/../appimage/ffmpeg-prefix;`pwd`/../appimage/sdl2-prefix;/opt/qt512" \ + "-DCMAKE_PREFIX_PATH=`pwd`/../appimage/ffmpeg-prefix;`pwd`/../appimage/sdl2-prefix" \ -DCHIAKI_ENABLE_TESTS=ON \ -DCHIAKI_ENABLE_CLI=OFF \ -DCHIAKI_ENABLE_GUI=ON \ @@ -40,9 +38,6 @@ curl -L -O https://github.com/linuxdeploy/linuxdeploy/releases/download/continuo chmod +x linuxdeploy-x86_64.AppImage curl -L -O https://github.com/linuxdeploy/linuxdeploy-plugin-qt/releases/download/continuous/linuxdeploy-plugin-qt-x86_64.AppImage chmod +x linuxdeploy-plugin-qt-x86_64.AppImage -set +e -source /opt/qt512/bin/qt512-env.sh -set -e export LD_LIBRARY_PATH="`pwd`/sdl2-prefix/lib:$LD_LIBRARY_PATH" export EXTRA_QT_PLUGINS=opengl diff --git a/scripts/build-ffmpeg.sh b/scripts/build-ffmpeg.sh index d00b962..9b6348f 100755 --- a/scripts/build-ffmpeg.sh +++ b/scripts/build-ffmpeg.sh @@ -5,7 +5,7 @@ cd "./$1" shift ROOT="`pwd`" -TAG=n4.3.1 +TAG=n4.3.9 git clone https://git.ffmpeg.org/ffmpeg.git --depth 1 -b $TAG && cd ffmpeg || exit 1 diff --git a/scripts/fetch-protoc.sh b/scripts/fetch-protoc.sh deleted file mode 100755 index e1d2d2f..0000000 --- a/scripts/fetch-protoc.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/bash - -set -xe - -cd $(dirname "${BASH_SOURCE[0]}")/.. -cd "./$1" -ROOT="`pwd`" - -URL=https://github.com/protocolbuffers/protobuf/releases/download/v3.9.1/protoc-3.9.1-linux-x86_64.zip - -curl -L "$URL" -o protoc.zip -unzip protoc.zip -d protoc - diff --git a/scripts/kitware-archive-latest.asc b/scripts/kitware-archive-latest.asc deleted file mode 100644 index 2c95d3e..0000000 --- a/scripts/kitware-archive-latest.asc +++ /dev/null @@ -1,64 +0,0 @@ ------BEGIN PGP PUBLIC KEY BLOCK----- - -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 -=8BHh ------END PGP PUBLIC KEY BLOCK----- diff --git a/scripts/run-podman-build-appimage.sh b/scripts/run-podman-build-appimage.sh index 5dea4b4..5024952 100755 --- a/scripts/run-podman-build-appimage.sh +++ b/scripts/run-podman-build-appimage.sh @@ -3,13 +3,14 @@ set -xe cd "`dirname $(readlink -f ${0})`" -podman build -t chiaki-bionic . -f Dockerfile.bionic +podman build --arch amd64 -t localhost/chiaki-noble . -f Dockerfile.noble cd .. podman run --rm \ + --arch amd64 \ -v "`pwd`:/build/chiaki" \ -w "/build/chiaki" \ --device /dev/fuse \ --cap-add SYS_ADMIN \ - -t chiaki-bionic \ + -t localhost/chiaki-noble \ /bin/bash -c "scripts/build-appimage.sh /build/appdir"