From 835f570ad9de2280adf4f9e59f4b5d4b035b559e Mon Sep 17 00:00:00 2001 From: ABeltramo Date: Sun, 8 Jan 2023 13:45:34 +0000 Subject: [PATCH 01/25] fix: static release build --- CMakeLists.txt | 2 +- src/helpers/CMakeLists.txt | 13 ++++--------- src/streaming/streaming/gst-plugin/audio.hpp | 2 +- src/streaming/streaming/gst-plugin/video.hpp | 2 +- src/streaming/streaming/streaming.hpp | 2 +- 5 files changed, 8 insertions(+), 13 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 52337c72..d85d5fd2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -18,7 +18,7 @@ if (CMAKE_PROJECT_NAME STREQUAL PROJECT_NAME) # Let's nicely support folders in IDEs set_property(GLOBAL PROPERTY USE_FOLDERS ON) - set(BUILD_SHARED_LIBS ON) + option(BUILD_SHARED_LIBS "Build libs as shared" ON) # set(Boost_USE_STATIC_LIBS ON) # Testing only available if this is the main app diff --git a/src/helpers/CMakeLists.txt b/src/helpers/CMakeLists.txt index 8671e3ba..5da35754 100644 --- a/src/helpers/CMakeLists.txt +++ b/src/helpers/CMakeLists.txt @@ -1,14 +1,9 @@ -# Optionally glob, but only for CMake 3.12 or later: -file(GLOB HEADER_LIST CONFIGURE_DEPENDS helpers/*.hpp) - # Make an automatic library - will be static or dynamic based on user setting -add_library(wolf_helpers) +add_library(wolf_helpers INTERFACE) # header only library needs to be set as INTERFACE add_library(wolf::helpers ALIAS wolf_helpers) -target_sources(wolf_helpers PUBLIC ${HEADER_LIST}) - # We need this directory, and users of our library will need it too -target_include_directories(wolf_helpers PUBLIC .) +target_include_directories(wolf_helpers INTERFACE .) set_target_properties(wolf_helpers PROPERTIES PUBLIC_HEADER .) set_target_properties(wolf_helpers PROPERTIES OUTPUT_NAME "helpers") @@ -30,11 +25,11 @@ FetchContent_MakeAvailable(fmtlib) find_package(Boost REQUIRED COMPONENTS log_setup log) include_directories(${Boost_INCLUDE_DIRS}) -target_link_libraries(wolf_helpers PUBLIC +target_link_libraries(wolf_helpers INTERFACE ${Boost_LIBRARIES} fmt::fmt-header-only range-v3::range-v3) # All users of this library will need at least C++17 -target_compile_features(wolf_helpers PUBLIC cxx_std_17) +target_compile_features(wolf_helpers INTERFACE cxx_std_17) set_target_properties(wolf_helpers PROPERTIES LINKER_LANGUAGE CXX) diff --git a/src/streaming/streaming/gst-plugin/audio.hpp b/src/streaming/streaming/gst-plugin/audio.hpp index 415bdc82..01783648 100644 --- a/src/streaming/streaming/gst-plugin/audio.hpp +++ b/src/streaming/streaming/gst-plugin/audio.hpp @@ -64,7 +64,7 @@ static GstBuffer *create_rtp_fec_header(const gst_rtp_moonlight_pay_audio &rtpmo return buf; } -GstBuffer *create_rtp_audio_buffer(const gst_rtp_moonlight_pay_audio &rtpmoonlightpay, GstBuffer *inbuf) { +static GstBuffer *create_rtp_audio_buffer(const gst_rtp_moonlight_pay_audio &rtpmoonlightpay, GstBuffer *inbuf) { GstBuffer *payload = inbuf; if (rtpmoonlightpay.encrypt) { diff --git a/src/streaming/streaming/gst-plugin/video.hpp b/src/streaming/streaming/gst-plugin/video.hpp index 1e3c789e..a54de16b 100644 --- a/src/streaming/streaming/gst-plugin/video.hpp +++ b/src/streaming/streaming/gst-plugin/video.hpp @@ -50,7 +50,7 @@ create_rtp_header(const gst_rtp_moonlight_pay_video &rtpmoonlightpay, int packet return buf; } -GstBuffer *prepend_video_header(GstBuffer *inbuf) { +static GstBuffer *prepend_video_header(GstBuffer *inbuf) { constexpr auto video_payload_header_size = 8; GstBuffer *video_header = gst_buffer_new_and_fill(video_payload_header_size, "\0017charss"); auto full_payload_buf = gst_buffer_append(video_header, inbuf); diff --git a/src/streaming/streaming/streaming.hpp b/src/streaming/streaming/streaming.hpp index d57d116e..83306a7a 100644 --- a/src/streaming/streaming/streaming.hpp +++ b/src/streaming/streaming/streaming.hpp @@ -14,7 +14,7 @@ void init(); /** * @return the Gstreamer version we are linked to */ -std::string version() { +inline std::string version() { guint major, minor, micro, nano; gst_version(&major, &minor, µ, &nano); return fmt::format("{}.{}.{}-{}", major, minor, micro, nano); From 00b4c51879d8016a6af5be9ef443a2c685c3b803 Mon Sep 17 00:00:00 2001 From: ABeltramo Date: Sun, 8 Jan 2023 20:43:58 +0000 Subject: [PATCH 02/25] feat: basic Dockerfile, setting certificates paths via env --- .dockerignore | 5 +++ Dockerfile | 72 ++++++++++++++++++++++++++++++++++++++ src/helpers/CMakeLists.txt | 2 -- src/wolf/wolf.cpp | 10 +++--- 4 files changed, 83 insertions(+), 6 deletions(-) create mode 100644 .dockerignore create mode 100644 Dockerfile diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..204a96d0 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,5 @@ +cmake-build* +docs/ +.github/ +.dockerignore +Dockerfile \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..2d3a7f44 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,72 @@ +######################################################## +FROM ubuntu:22.04 AS wolf-builder + +ENV DEBIAN_FRONTEND=noninteractive + +RUN apt-get update -y && \ + apt-get install -y --no-install-recommends \ + ca-certificates \ + ninja-build \ + cmake \ + git \ + clang \ + libboost-thread-dev libboost-filesystem-dev libboost-log-dev libboost-stacktrace-dev \ + libssl-dev \ + libgstreamer1.0-dev \ + libevdev-dev \ + libunwind-dev \ + && rm -rf /var/lib/apt/lists/* + +COPY . /wolf +WORKDIR /wolf + +RUN cmake -Bbuild \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_CXX_STANDARD=17 \ + -DCMAKE_CXX_EXTENSIONS=OFF \ + -DBUILD_SHARED_LIBS=OFF \ + -DBoost_USE_STATIC_LIBS=ON \ + -G Ninja && \ + ninja -C build + +######################################################## +# TODO: build gstreamer plugin manually +######################################################## +FROM ubuntu:22.04 AS runner +ENV DEBIAN_FRONTEND=noninteractive + +RUN apt-get update -y && \ + apt-get install -y --no-install-recommends \ + gstreamer1.0-plugins-base gstreamer1.0-plugins-good gstreamer1.0-plugins-ugly gstreamer1.0-plugins-bad \ + libssl3 \ + libevdev2 \ + && rm -rf /var/lib/apt/lists/* + +COPY --from=wolf-builder /wolf/build/src/wolf/wolf /wolf/wolf + +WORKDIR /wolf + +ARG WOLF_CFG_FOLDER=/wolf/cfg +ENV WOLF_CFG_FOLDER=$WOLF_CFG_FOLDER +RUN mkdir $WOLF_CFG_FOLDER + +ENV LOG_LEVEL=INFO +ENV CFG_FILE=$WOLF_CFG_FOLDER/config.toml +ENV PRIVATE_KEY_FILE=$WOLF_CFG_FOLDER/key.pem +ENV PRIVATE_CERT_FILE=$WOLF_CFG_FOLDER/cert.pem +VOLUME $WOLF_CFG_FOLDER + +# HTTPS +EXPOSE 47984/tcp +# HTTP +EXPOSE 47989/tcp +# Video +EXPOSE 47998/udp +# Control +EXPOSE 47999/udp +# Audio +EXPOSE 48000/udp +# RTSP +EXPOSE 48010/tcp + +CMD ["/wolf/wolf"] \ No newline at end of file diff --git a/src/helpers/CMakeLists.txt b/src/helpers/CMakeLists.txt index 5da35754..d93a8e29 100644 --- a/src/helpers/CMakeLists.txt +++ b/src/helpers/CMakeLists.txt @@ -5,7 +5,6 @@ add_library(wolf::helpers ALIAS wolf_helpers) # We need this directory, and users of our library will need it too target_include_directories(wolf_helpers INTERFACE .) set_target_properties(wolf_helpers PROPERTIES PUBLIC_HEADER .) -set_target_properties(wolf_helpers PROPERTIES OUTPUT_NAME "helpers") # Additional algorithms for dealing with containers FetchContent_Declare( @@ -32,4 +31,3 @@ target_link_libraries(wolf_helpers INTERFACE # All users of this library will need at least C++17 target_compile_features(wolf_helpers INTERFACE cxx_std_17) -set_target_properties(wolf_helpers PROPERTIES LINKER_LANGUAGE CXX) diff --git a/src/wolf/wolf.cpp b/src/wolf/wolf.cpp index 35ba0e7e..6ae24f17 100644 --- a/src/wolf/wolf.cpp +++ b/src/wolf/wolf.cpp @@ -42,11 +42,11 @@ state::Host get_host_config(std::string_view pkey_filename, std::string_view cer X509 *server_cert; EVP_PKEY *server_pkey; if (x509::cert_exists(pkey_filename, cert_filename)) { - logs::log(logs::debug, "Loading server certificates from disk..."); + logs::log(logs::debug, "Loading server certificates from disk: {} {}", cert_filename, pkey_filename); server_cert = x509::cert_from_file(cert_filename); server_pkey = x509::pkey_from_file(pkey_filename); } else { - logs::log(logs::info, "x509 certificates not present, generating..."); + logs::log(logs::info, "x509 certificates not present, generating: {} {}", cert_filename, pkey_filename); server_pkey = x509::generate_key(); server_cert = x509::generate_x509(server_pkey); x509::write_to_disk(server_pkey, pkey_filename, server_cert, cert_filename); @@ -226,13 +226,15 @@ int main(int argc, char *argv[]) { check_exceptions(); auto config_file = get_env("CFG_FILE", "config.toml"); - auto local_state = initialize(config_file, "key.pem", "cert.pem"); + auto p_key_file = get_env("PRIVATE_KEY_FILE", "key.pem"); + auto p_cert_file = get_env("PRIVATE_CERT_FILE", "cert.pem"); + auto local_state = initialize(config_file, p_key_file, p_cert_file); // REST HTTP/S APIs auto http_server = std::make_unique(); auto http_thread = HTTPServers::startServer(http_server.get(), local_state, state::HTTP_PORT); - auto https_server = std::make_unique("cert.pem", "key.pem"); + auto https_server = std::make_unique(p_cert_file, p_key_file); auto https_thread = HTTPServers::startServer(https_server.get(), local_state, state::HTTPS_PORT); // Holds reference to all running threads, grouped by session_id From 4eb797dd1426b2fbf560fb39bb2b1cd2fb8a1857 Mon Sep 17 00:00:00 2001 From: ABeltramo Date: Sun, 8 Jan 2023 21:01:12 +0000 Subject: [PATCH 03/25] feat: using buildkit cache to speed up builds --- Dockerfile | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index 2d3a7f44..8df18cf0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,6 +8,7 @@ RUN apt-get update -y && \ ca-certificates \ ninja-build \ cmake \ + ccache \ git \ clang \ libboost-thread-dev libboost-filesystem-dev libboost-log-dev libboost-stacktrace-dev \ @@ -20,14 +21,21 @@ RUN apt-get update -y && \ COPY . /wolf WORKDIR /wolf -RUN cmake -Bbuild \ +ENV CCACHE_DIR=/cache/ccache +ENV CMAKE_BUILD_DIR=/cache/cmake-build +RUN --mount=type=cache,target=/cache/ccache \ + --mount=type=cache,target=/cache/cmake-build \ + cmake -B$CMAKE_BUILD_DIR \ -DCMAKE_BUILD_TYPE=Release \ -DCMAKE_CXX_STANDARD=17 \ -DCMAKE_CXX_EXTENSIONS=OFF \ -DBUILD_SHARED_LIBS=OFF \ -DBoost_USE_STATIC_LIBS=ON \ + -DBUILD_TESTING=OFF \ -G Ninja && \ - ninja -C build + ninja -C $CMAKE_BUILD_DIR && \ + # We have to copy out the built executable because this will only be available inside the buildkit cache + cp $CMAKE_BUILD_DIR/src/wolf/wolf /wolf/wolf ######################################################## # TODO: build gstreamer plugin manually @@ -42,7 +50,7 @@ RUN apt-get update -y && \ libevdev2 \ && rm -rf /var/lib/apt/lists/* -COPY --from=wolf-builder /wolf/build/src/wolf/wolf /wolf/wolf +COPY --from=wolf-builder /wolf/wolf /wolf/wolf WORKDIR /wolf From aba7d07119238e48b8d44c40751f0c28d645b988 Mon Sep 17 00:00:00 2001 From: ABeltramo Date: Fri, 13 Jan 2023 18:11:12 +0000 Subject: [PATCH 04/25] feat: added tini and curl to the final image --- .github/workflows/docker-build.yml | 0 Dockerfile | 8 +++++++- 2 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/docker-build.yml diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml new file mode 100644 index 00000000..e69de29b diff --git a/Dockerfile b/Dockerfile index 8df18cf0..0fbbc577 100644 --- a/Dockerfile +++ b/Dockerfile @@ -18,7 +18,9 @@ RUN apt-get update -y && \ libunwind-dev \ && rm -rf /var/lib/apt/lists/* -COPY . /wolf +COPY src /wolf/src +COPY cmake /wolf/cmake +COPY CMakeLists.txt /wolf/CMakeLists.txt WORKDIR /wolf ENV CCACHE_DIR=/cache/ccache @@ -43,9 +45,12 @@ RUN --mount=type=cache,target=/cache/ccache \ FROM ubuntu:22.04 AS runner ENV DEBIAN_FRONTEND=noninteractive +# curl only used by plugin curlhttpsrc (remote video play) RUN apt-get update -y && \ apt-get install -y --no-install-recommends \ gstreamer1.0-plugins-base gstreamer1.0-plugins-good gstreamer1.0-plugins-ugly gstreamer1.0-plugins-bad \ + ca-certificates libcurl4 \ + tini \ libssl3 \ libevdev2 \ && rm -rf /var/lib/apt/lists/* @@ -77,4 +82,5 @@ EXPOSE 48000/udp # RTSP EXPOSE 48010/tcp +ENTRYPOINT ["/usr/bin/tini", "--"] CMD ["/wolf/wolf"] \ No newline at end of file From fb54af4a9dee3d6ab948582b250839f446249582 Mon Sep 17 00:00:00 2001 From: ABeltramo Date: Fri, 13 Jan 2023 18:11:12 +0000 Subject: [PATCH 05/25] feat: added drakulix/sunrise gstreamer plugin --- Dockerfile | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/Dockerfile b/Dockerfile index 0fbbc577..12424f81 100644 --- a/Dockerfile +++ b/Dockerfile @@ -39,12 +39,35 @@ RUN --mount=type=cache,target=/cache/ccache \ # We have to copy out the built executable because this will only be available inside the buildkit cache cp $CMAKE_BUILD_DIR/src/wolf/wolf /wolf/wolf +######################################################## +FROM rust:1.66-slim AS gst-plugin-wayland + +ENV DEBIAN_FRONTEND=noninteractive +RUN apt-get update -y && \ + apt-get install -y --no-install-recommends \ + git ca-certificates pkg-config \ + libwayland-dev libwayland-server0 libudev-dev libinput-dev libxkbcommon-dev libgbm-dev \ + libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev \ + && rm -rf /var/lib/apt/lists/* + +ARG SUNRISE_SHA=eb5b07d79c42a69e700ca5666ce1710f9c8f4ef0 +ENV SUNRISE_SHA=$SUNRISE_SHA +RUN git clone https://github.com/Drakulix/sunrise.git && \ + cd sunrise && \ + git checkout $SUNRISE_SHA + +WORKDIR /sunrise/gst-plugin-wayland-display + +RUN --mount=type=cache,target=/usr/local/cargo/registry cargo build --release + + ######################################################## # TODO: build gstreamer plugin manually ######################################################## FROM ubuntu:22.04 AS runner ENV DEBIAN_FRONTEND=noninteractive +# Wolf runtime dependencies # curl only used by plugin curlhttpsrc (remote video play) RUN apt-get update -y && \ apt-get install -y --no-install-recommends \ @@ -55,7 +78,23 @@ RUN apt-get update -y && \ libevdev2 \ && rm -rf /var/lib/apt/lists/* +# gst-plugin-wayland runtime dependencies +RUN apt-get update -y && \ + apt-get install -y --no-install-recommends \ + libwayland-server0 libinput10 libxkbcommon0 libgbm1 \ + libglvnd0 libgl1 libglx0 libegl1 libgles2 \ + && rm -rf /var/lib/apt/lists/* + +# TODO: avoid running as root + +# DEBUG: install gstreamer1.0-tools in order to add gst-inspect-1.0, gst-launch-1.0 and more +ENV GST_PLUGIN_PATH=/usr/lib/x86_64-linux-gnu/gstreamer-1.0/ COPY --from=wolf-builder /wolf/wolf /wolf/wolf +COPY --from=gst-plugin-wayland /sunrise/gst-plugin-wayland-display/target/release/libgstwaylanddisplay.so $GST_PLUGIN_PATH/libgstwaylanddisplay.so + +# Here is where the dinamically created wayland sockets will be stored +ENV XDG_RUNTIME_DIR=/wolf/run/ +RUN mkdir $XDG_RUNTIME_DIR WORKDIR /wolf From fbb050a465124da6a94adf7d5281fd85f20a9c4b Mon Sep 17 00:00:00 2001 From: ABeltramo Date: Fri, 13 Jan 2023 20:21:10 +0000 Subject: [PATCH 06/25] feat: added spawn process on app start --- src/wolf/helpers/logger.hpp | 125 ----------------------------- src/wolf/helpers/utils.hpp | 39 --------- src/wolf/process/process.cpp | 64 +++++++++++++++ src/wolf/process/process.hpp | 23 ++++++ src/wolf/rest/custom-https.cpp | 7 +- src/wolf/state/configTOML.cpp | 9 ++- src/wolf/state/data-structures.hpp | 2 + src/wolf/wolf.cpp | 21 +++-- 8 files changed, 118 insertions(+), 172 deletions(-) delete mode 100644 src/wolf/helpers/logger.hpp delete mode 100644 src/wolf/helpers/utils.hpp create mode 100644 src/wolf/process/process.cpp create mode 100644 src/wolf/process/process.hpp diff --git a/src/wolf/helpers/logger.hpp b/src/wolf/helpers/logger.hpp deleted file mode 100644 index 4280290b..00000000 --- a/src/wolf/helpers/logger.hpp +++ /dev/null @@ -1,125 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace logs { - -using namespace boost::log::trivial; - -inline auto get_color(boost::log::trivial::severity_level level) { - switch (level) { - case debug: - case trace: - case info: - return "\033[37;1m"; - case warning: - return "\033[33;1m"; - case error: - case fatal: - return "\033[31;1m"; - default: - return "\033[0m"; - } -} - -inline auto get_name(boost::log::trivial::severity_level level) { - switch (level) { - case trace: - return "TRACE"; - break; - case debug: - return "DEBUG"; - break; - case info: - return "INFO"; - break; - case warning: - return "WARN"; - break; - case error: - return "ERROR"; - break; - case fatal: - return "FATAL"; - break; - default: - return ""; - break; - } -} - -/** - * @brief first time Boost log system initialization - * - * @param min_log_level: The minum log level to be reported, anything below this will not be printed - */ -inline void init(severity_level min_log_level) { - /* init boost log - * 1. Add common attributes - * 2. set log filter to trace - */ - boost::log::add_common_attributes(); - boost::log::core::get()->set_filter(boost::log::trivial::severity >= min_log_level); - - /* console sink */ - auto consoleSink = boost::log::add_console_log(std::clog); - consoleSink->set_formatter([](boost::log::record_view const &rec, boost::log::formatting_ostream &strm) { - auto severity = rec[boost::log::trivial::severity]; - auto msg = rec[boost::log::expressions::smessage]; - auto now = std::chrono::system_clock::now(); - - strm << get_color(severity.get()); - strm << fmt::format("{:%T} {:<5} | {}", now.time_since_epoch(), get_name(severity.get()), msg.get()); - strm << "\033[0m"; - }); -} - -/** - * @brief output a log message with optional format - * - * @param lv: log level - * @param format_str: a valid fmt::format string - * @param args: optional additional args to be formatted - */ -template inline void log(severity_level lv, const S &format_str, const Args &...args) { - auto msg = fmt::format(format_str, args...); - boost::log::sources::severity_logger lg; - - BOOST_LOG_SEV(lg, lv) << msg; -} - -inline logs::severity_level parse_level(const std::string &level) { - std::string lvl = level; - std::transform(level.begin(), level.end(), lvl.begin(), [](unsigned char c) { return std::toupper(c); }); - if (lvl == "TRACE") { - return logs::trace; - } else if (lvl == "DEBUG") { - return logs::debug; - } else if (lvl == "INFO") { - return logs::info; - } else if (lvl == "WARNING") { - return logs::warning; - } else if (lvl == "ERROR") { - return logs::error; - } else { - return logs::fatal; - } -} -} // namespace logs \ No newline at end of file diff --git a/src/wolf/helpers/utils.hpp b/src/wolf/helpers/utils.hpp deleted file mode 100644 index c6b7e564..00000000 --- a/src/wolf/helpers/utils.hpp +++ /dev/null @@ -1,39 +0,0 @@ -#pragma once -#include -#include - -namespace utils { - -using namespace ranges; - -/** - * Since we can't switch() on strings we use hashes instead. - * Adapted from https://stackoverflow.com/a/46711735/3901988 - */ -constexpr uint32_t hash(const std::string_view data) noexcept { - uint32_t hash = 5385; - for (const auto &e : data) - hash = ((hash << 5) + hash) + e; - return hash; -} - -/** - * Returns the sub_string between the two input characters - */ -inline std::string_view sub_string(std::string_view str, char begin, char end) { - auto s = str.find(begin); - auto e = str.find(end, s); - return str.substr(s + 1, e - s - 1); -} - -/** - * Splits the given string into an array of strings at any given separator - */ -inline std::vector split(std::string_view str, char separator) { - return str // - | views::split(separator) // - | views::transform([](auto &&ptrs) { return std::string_view(&*ptrs.begin(), distance(ptrs)); }) // - | to_vector; // -} - -} // namespace utils \ No newline at end of file diff --git a/src/wolf/process/process.cpp b/src/wolf/process/process.cpp new file mode 100644 index 00000000..b795f6c1 --- /dev/null +++ b/src/wolf/process/process.cpp @@ -0,0 +1,64 @@ +#include +#include +#include +#include +#include +#include +#include + +namespace process { + +using namespace moonlight; + +void run_process(immer::box process_ev) { + logs::log(logs::debug, "[PROCESS] Starting process: {}", process_ev->run_cmd); + + std::future std_out, err_out; + boost::asio::io_service ios; + bp::child child_proc; + bp::group group_proc; + + try { + child_proc = bp::child(process_ev->run_cmd, + bp::std_in.close(), + bp::std_out > std_out, + bp::std_err > err_out, + ios, + group_proc); + } catch (const std::system_error &e) { + logs::log(logs::error, "Unable to start process, error: {} - {}", e.code().value(), e.what()); + return; + } + + auto client_connected = immer::atom(true); + auto terminate_handler = process_ev->event_bus->register_handler>( + [&client_connected, &group_proc, sess_id = process_ev->session_id]( + immer::box terminate_ev) { + if (terminate_ev->session_id == sess_id) { + client_connected.store(false); + group_proc.terminate(); // Manually terminate the process + } + }); + + ios.run(); // This will stop here until the process is over + + if (*client_connected.load()) { + // TODO: the process terminated in error before the user closed the connection + // We should signal this to the control event so that it can close the connection downstream + // fire_event( ... ) + } + child_proc.wait(); // to avoid a zombie process & get the exit code + + auto ex_code = child_proc.exit_code(); + logs::log(logs::debug, "[PROCESS] Terminated with status code: {}\nstd_out: {}", ex_code, std_out.get()); + if (!err_out.get().empty()) { + logs::log(logs::warning, "[PROCESS] Terminated with status code: {}, std_err: {}", ex_code, err_out.get()); + } + + terminate_handler.unregister(); +} + +std::thread spawn_process(immer::box process_ev) { + return std::thread(run_process, process_ev); +} +} // namespace process \ No newline at end of file diff --git a/src/wolf/process/process.hpp b/src/wolf/process/process.hpp new file mode 100644 index 00000000..a27bc119 --- /dev/null +++ b/src/wolf/process/process.hpp @@ -0,0 +1,23 @@ +#pragma once +#include +#include +#include +#include +#include + +namespace process { + +namespace bp = boost::process; + +struct START_PROCESS_EV { + std::size_t session_id; + std::shared_ptr event_bus; + + std::string run_cmd; +}; + +std::thread spawn_process(immer::box process_ev); + +void run_process(immer::box process_ev); + +} // namespace process \ No newline at end of file diff --git a/src/wolf/rest/custom-https.cpp b/src/wolf/rest/custom-https.cpp index b9b5221a..f4afedea 100644 --- a/src/wolf/rest/custom-https.cpp +++ b/src/wolf/rest/custom-https.cpp @@ -16,7 +16,12 @@ Server::Server(const std::string &certification_file, const std::string & }); this->on_error = [](std::shared_ptr::Request> request, const error_code &ec) -> void { - logs::log(logs::warning, "HTTPS error during request at {} error code: {}", request->path, ec.message()); + // TODO: why is stream truncated happening so frequently? + logs::log(ec.value() == 1 ? logs::trace : logs::warning, + "HTTPS error during request at {} error code: {} - {}", + request->path, + ec.value(), + ec.message()); return; }; } diff --git a/src/wolf/state/configTOML.cpp b/src/wolf/state/configTOML.cpp index 7a5b3403..d433b877 100644 --- a/src/wolf/state/configTOML.cpp +++ b/src/wolf/state/configTOML.cpp @@ -63,7 +63,8 @@ Config get_default() { .hevc_gst_pipeline = video::DEFAULT_SOURCE.data() + " ! "s + video::DEFAULT_PARAMS.data() + " ! "s + video::DEFAULT_H265_ENCODER.data() + " ! " + video::DEFAULT_SINK.data(), .opus_gst_pipeline = audio::DEFAULT_SOURCE.data() + " ! "s + audio::DEFAULT_PARAMS.data() + " ! "s + - audio::DEFAULT_OPUS_ENCODER.data() + " ! " + audio::DEFAULT_SINK.data()}}}; + audio::DEFAULT_OPUS_ENCODER.data() + " ! " + audio::DEFAULT_SINK.data(), + .run_cmd = "sh -c \"while :; do echo 'running...'; sleep 1; done\""}}}; } Config load_or_default(const std::string &source) { @@ -129,12 +130,16 @@ Config load_or_default(const std::string &source) { + " ! " + // toml::find_or(item, "audio", "sink", default_gst_audio_settings.default_sink); + auto run_cmd = + toml::find_or(item, "run_cmd", "sh -c \"while :; do echo 'running...'; sleep 1; done\""); + return state::App{.base = {.title = toml::find(item, "title"), .id = std::to_string(idx + 1), // Moonlight expects: 1,2,3 ... .support_hdr = toml::find_or(item, "support_hdr", false)}, .h264_gst_pipeline = h264_gst_pipeline, .hevc_gst_pipeline = hevc_gst_pipeline, - .opus_gst_pipeline = opus_gst_pipeline}; + .opus_gst_pipeline = opus_gst_pipeline, + .run_cmd = run_cmd}; }) // | ranges::to>(); // diff --git a/src/wolf/state/data-structures.hpp b/src/wolf/state/data-structures.hpp index 1a187c61..f8b887f3 100644 --- a/src/wolf/state/data-structures.hpp +++ b/src/wolf/state/data-structures.hpp @@ -93,6 +93,8 @@ struct App { std::string h264_gst_pipeline; std::string hevc_gst_pipeline; std::string opus_gst_pipeline; + + std::string run_cmd; }; /** diff --git a/src/wolf/wolf.cpp b/src/wolf/wolf.cpp index 6ae24f17..90dfada0 100644 --- a/src/wolf/wolf.cpp +++ b/src/wolf/wolf.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -136,11 +137,21 @@ auto setup_sessions_handlers(std::shared_ptr &event_bus, TreadsMa // RTSP auto rtsp_launch_sig = event_bus->register_handler>( [&threads](immer::box stream_session) { - auto thread = rtsp::start_server(stream_session->rtsp_port, stream_session); - auto thread_ptr = std::make_unique(std::move(thread)); - - threads.update([&thread_ptr, sess_id = stream_session->session_id](auto t_map) { - return t_map.update(sess_id, [&thread_ptr](auto t_vec) { return t_vec.push_back(std::move(thread_ptr)); }); + // Start RTSP + auto rtsp_thread = rtsp::start_server(stream_session->rtsp_port, stream_session); + auto rtsp_thread_ptr = std::make_unique(std::move(rtsp_thread)); + + // Start app process + auto process_thread = process::spawn_process(process::START_PROCESS_EV{.session_id = stream_session->session_id, + .event_bus = stream_session->event_bus, + .run_cmd = stream_session->app.run_cmd}); + auto process_thread_ptr = std::make_unique(std::move(process_thread)); + + threads.update([&rtsp_thread_ptr, &process_thread_ptr, sess_id = stream_session->session_id](auto t_map) { + return t_map.update(sess_id, [&rtsp_thread_ptr, &process_thread_ptr](auto t_vec) { + // TODO: use transient instead + return t_vec.push_back(std::move(rtsp_thread_ptr)).push_back(std::move(process_thread_ptr)); + }); }); }); From 84e4338d246997a1025279786e23dd1bb02a5023 Mon Sep 17 00:00:00 2001 From: ABeltramo Date: Sun, 15 Jan 2023 11:28:23 +0000 Subject: [PATCH 07/25] feat: thread pool, decoupled inputs with control --- src/input/input/input.hpp | 8 +- src/input/platforms/linux/uinput.cpp | 13 ++- src/input/platforms/unknown/input.cpp | 5 +- src/wolf/control/control.cpp | 126 +++++++++++------------ src/wolf/control/control.hpp | 2 +- src/wolf/process/process.cpp | 9 +- src/wolf/process/process.hpp | 17 ++-- src/wolf/rtsp/commands.hpp | 12 ++- src/wolf/rtsp/net.hpp | 48 ++++----- src/wolf/state/configTOML.cpp | 1 + src/wolf/wolf.cpp | 138 +++++++++++++------------- 11 files changed, 192 insertions(+), 187 deletions(-) diff --git a/src/input/input/input.hpp b/src/input/input/input.hpp index b188ae34..b73767d0 100644 --- a/src/input/input/input.hpp +++ b/src/input/input/input.hpp @@ -10,12 +10,16 @@ namespace input { +struct InputReady { + immer::array> devices_paths; + immer::array> registered_handlers; +}; + /** * PLATFORM DEPENDENT * will wait for events on the event bus and setup virtual devices accordingly. */ -immer::array> setup_handlers(std::size_t session_id, - const std::shared_ptr& event_bus); +InputReady setup_handlers(std::size_t session_id, const std::shared_ptr &event_bus); /** * A packet of type INPUT_DATA will have different shapes based on the type diff --git a/src/input/platforms/linux/uinput.cpp b/src/input/platforms/linux/uinput.cpp index 0d54d438..d79f5042 100644 --- a/src/input/platforms/linux/uinput.cpp +++ b/src/input/platforms/linux/uinput.cpp @@ -15,6 +15,7 @@ #include #include #include +#include #include #include #include @@ -382,31 +383,35 @@ void controller_handle(libevdev_uinput *controller, } // namespace controller -immer::array> setup_handlers(std::size_t session_id, - const std::shared_ptr &event_bus) { +InputReady setup_handlers(std::size_t session_id, const std::shared_ptr &event_bus) { logs::log(logs::debug, "Setting up input handlers for session: {}", session_id); auto v_devices = std::make_shared(); + auto devices_paths = immer::array>().transient(); libevdev_ptr mouse_dev(libevdev_new(), ::libevdev_free); if (auto mouse_el = mouse::create_mouse(mouse_dev.get())) { v_devices->mouse = {*mouse_el, ::libevdev_uinput_destroy}; + devices_paths.push_back(libevdev_uinput_get_devnode(*mouse_el)); } libevdev_ptr touch_dev(libevdev_new(), ::libevdev_free); if (auto touch_el = mouse::create_touchpad(touch_dev.get())) { v_devices->touchpad = {*touch_el, ::libevdev_uinput_destroy}; + devices_paths.push_back(libevdev_uinput_get_devnode(*touch_el)); } libevdev_ptr keyboard_dev(libevdev_new(), ::libevdev_free); if (auto keyboard_el = keyboard::create_keyboard(keyboard_dev.get())) { v_devices->keyboard = {*keyboard_el, ::libevdev_uinput_destroy}; + devices_paths.push_back(libevdev_uinput_get_devnode(*keyboard_el)); } // TODO: multiple controllers? libevdev_ptr controller_dev(libevdev_new(), ::libevdev_free); if (auto controller_el = controller::create_controller(controller_dev.get())) { v_devices->controllers = {{*controller_el, ::libevdev_uinput_destroy}}; + devices_paths.push_back(libevdev_uinput_get_devnode(*controller_el)); } auto controller_state = std::make_shared /* prev packet */>>(); @@ -514,6 +519,8 @@ immer::array> setup_handlers(std::size_t se } }); - return immer::array>{std::move(ctrl_handler), std::move(end_handler)}; + return InputReady{.devices_paths = devices_paths.persistent(), + .registered_handlers = immer::array>{std::move(ctrl_handler), + std::move(end_handler)}}; } } // namespace input \ No newline at end of file diff --git a/src/input/platforms/unknown/input.cpp b/src/input/platforms/unknown/input.cpp index e2e9c8db..a3bd7152 100644 --- a/src/input/platforms/unknown/input.cpp +++ b/src/input/platforms/unknown/input.cpp @@ -3,9 +3,8 @@ namespace input { -immer::array> setup_handlers(std::size_t session_id, - const std::shared_ptr &event_bus) { +InputReady setup_handlers(std::size_t session_id, const std::shared_ptr &event_bus) { logs::log(logs::error, "Unable to setup input handlers for the current platform."); - return {}; + return {.devices_paths = {}, .registered_handlers = {}}; } } // namespace input \ No newline at end of file diff --git a/src/wolf/control/control.cpp b/src/wolf/control/control.cpp index 09c58f83..6460091f 100644 --- a/src/wolf/control/control.cpp +++ b/src/wolf/control/control.cpp @@ -1,6 +1,7 @@ #include "input/input.hpp" #include #include +#include #include namespace control { @@ -67,78 +68,69 @@ std::pair get_ip(const sockaddr *const ip_ return {std::string{data}, port}; } -std::thread start_service(immer::box control_sess, int timeout_millis) { - return std::thread( - [timeout_millis](immer::box control_sess) { - enet_host host = create_host(control_sess->host, control_sess->port, control_sess->peers); - logs::log(logs::info, "Control server started on port: {}", control_sess->port); - - auto input_handlers = input::setup_handlers(control_sess->session_id, control_sess->event_bus); - - ENetEvent event; - bool terminated = false; - while (!terminated && enet_host_service(host.get(), &event, timeout_millis) > 0) { - auto [client_ip, client_port] = get_ip((sockaddr *)&event.peer->address.address); - - switch (event.type) { - case ENET_EVENT_TYPE_NONE: - break; - case ENET_EVENT_TYPE_CONNECT: - logs::log(logs::debug, "[ENET] connected client: {}:{}", client_ip, client_port); - break; - case ENET_EVENT_TYPE_DISCONNECT: - logs::log(logs::debug, "[ENET] disconnected client: {}:{}", client_ip, client_port); +void run_control(immer::box control_sess, int timeout_millis) { + + enet_host host = create_host(control_sess->host, control_sess->port, control_sess->peers); + logs::log(logs::info, "Control server started on port: {}", control_sess->port); + + ENetEvent event; + bool terminated = false; + while (!terminated && enet_host_service(host.get(), &event, timeout_millis) > 0) { + auto [client_ip, client_port] = get_ip((sockaddr *)&event.peer->address.address); + + switch (event.type) { + case ENET_EVENT_TYPE_NONE: + break; + case ENET_EVENT_TYPE_CONNECT: + logs::log(logs::debug, "[ENET] connected client: {}:{}", client_ip, client_port); + break; + case ENET_EVENT_TYPE_DISCONNECT: + logs::log(logs::debug, "[ENET] disconnected client: {}:{}", client_ip, client_port); + terminated = true; + break; + case ENET_EVENT_TYPE_RECEIVE: + enet_packet packet = {event.packet, enet_packet_destroy}; + + auto type = get_type(packet->data); + + logs::log(logs::trace, + "[ENET] received {} of {} bytes from: {}:{} HEX: {}", + packet_type_to_str(type), + packet->dataLength, + client_ip, + client_port, + crypto::str_to_hex({(char *)packet->data, packet->dataLength})); + + if (type == ENCRYPTED) { + try { + auto decrypted = decrypt_packet(packet->data, control_sess->aes_key); + + auto sub_type = get_type(reinterpret_cast(decrypted.data())); + + logs::log(logs::trace, + "[ENET] decrypted sub_type: {} HEX: {}", + packet_type_to_str(sub_type), + crypto::str_to_hex(decrypted)); + + if (sub_type == TERMINATION) { terminated = true; - break; - case ENET_EVENT_TYPE_RECEIVE: - enet_packet packet = {event.packet, enet_packet_destroy}; - - auto type = get_type(packet->data); - - logs::log(logs::trace, - "[ENET] received {} of {} bytes from: {}:{} HEX: {}", - packet_type_to_str(type), - packet->dataLength, - client_ip, - client_port, - crypto::str_to_hex({(char *)packet->data, packet->dataLength})); - - if (type == ENCRYPTED) { - try { - auto decrypted = decrypt_packet(packet->data, control_sess->aes_key); - - auto sub_type = get_type(reinterpret_cast(decrypted.data())); - - logs::log(logs::trace, - "[ENET] decrypted sub_type: {} HEX: {}", - packet_type_to_str(sub_type), - crypto::str_to_hex(decrypted)); - - if (sub_type == TERMINATION) { - terminated = true; - } - - auto ev = ControlEvent{control_sess->session_id, sub_type, decrypted}; - control_sess->event_bus->fire_event(immer::box{ev}); - } catch (std::runtime_error &e) { - logs::log(logs::error, "[ENET] Unable to decrypt incoming packet: {}", e.what()); - } - } - break; } - } - // Failsafe, when we get out of the loop we have to signal to terminate the session - logs::log(logs::debug, "[ENET] terminating session: {}", control_sess->session_id); + auto ev = ControlEvent{control_sess->session_id, sub_type, decrypted}; + control_sess->event_bus->fire_event(immer::box{ev}); + } catch (std::runtime_error &e) { + logs::log(logs::error, "[ENET] Unable to decrypt incoming packet: {}", e.what()); + } + } + break; + } + } - auto terminate_ev = TerminateEvent{control_sess->session_id}; - control_sess->event_bus->fire_event(immer::box{terminate_ev}); + // Failsafe, when we get out of the loop we have to signal to terminate the session + logs::log(logs::debug, "[ENET] terminating session: {}", control_sess->session_id); - for (const auto &handler : input_handlers) { - handler->unregister(); // This should also automagically remove the created virtual devices - } - }, - std::move(control_sess)); + auto terminate_ev = TerminateEvent{control_sess->session_id}; + control_sess->event_bus->fire_event(immer::box{terminate_ev}); } } // namespace control \ No newline at end of file diff --git a/src/wolf/control/control.hpp b/src/wolf/control/control.hpp index d0001808..a91522d3 100644 --- a/src/wolf/control/control.hpp +++ b/src/wolf/control/control.hpp @@ -7,7 +7,7 @@ namespace control { -std::thread start_service(immer::box control_sess, int timeout_millis = 1000); +void run_control(immer::box control_sess, int timeout_millis = 1000); bool init(); diff --git a/src/wolf/process/process.cpp b/src/wolf/process/process.cpp index b795f6c1..f6e54f85 100644 --- a/src/wolf/process/process.cpp +++ b/src/wolf/process/process.cpp @@ -10,8 +10,8 @@ namespace process { using namespace moonlight; -void run_process(immer::box process_ev) { - logs::log(logs::debug, "[PROCESS] Starting process: {}", process_ev->run_cmd); +void run_process(immer::box process_ev) { + logs::log(logs::debug, "[PROCESS] Starting process: {}", process_ev->app_launch_cmd); std::future std_out, err_out; boost::asio::io_service ios; @@ -19,7 +19,7 @@ void run_process(immer::box process_ev) { bp::group group_proc; try { - child_proc = bp::child(process_ev->run_cmd, + child_proc = bp::child(process_ev->app_launch_cmd, bp::std_in.close(), bp::std_out > std_out, bp::std_err > err_out, @@ -58,7 +58,4 @@ void run_process(immer::box process_ev) { terminate_handler.unregister(); } -std::thread spawn_process(immer::box process_ev) { - return std::thread(run_process, process_ev); -} } // namespace process \ No newline at end of file diff --git a/src/wolf/process/process.hpp b/src/wolf/process/process.hpp index a27bc119..bfb04911 100644 --- a/src/wolf/process/process.hpp +++ b/src/wolf/process/process.hpp @@ -2,22 +2,27 @@ #include #include #include +#include #include #include namespace process { -namespace bp = boost::process; - -struct START_PROCESS_EV { +/** + * This event will trigger the start of the application command + */ +struct LaunchAPPEvent { std::size_t session_id; std::shared_ptr event_bus; - std::string run_cmd; + /** + * The full command to be launched + */ + std::string app_launch_cmd; }; -std::thread spawn_process(immer::box process_ev); +namespace bp = boost::process; -void run_process(immer::box process_ev); +void run_process(immer::box process_ev); } // namespace process \ No newline at end of file diff --git a/src/wolf/rtsp/commands.hpp b/src/wolf/rtsp/commands.hpp index 78592c2e..1e8f84ef 100644 --- a/src/wolf/rtsp/commands.hpp +++ b/src/wolf/rtsp/commands.hpp @@ -2,6 +2,7 @@ #include #include +#include #include #include #include @@ -78,7 +79,14 @@ RTSP_PACKET setup(const RTSP_PACKET &req, const state::StreamSession &session) { {{"Session", session_opt}, {"Transport", "server_port=" + std::to_string(service_port)}}); } -RTSP_PACKET play(const RTSP_PACKET &req) { +RTSP_PACKET play(const RTSP_PACKET &req, const state::StreamSession &session) { + process::LaunchAPPEvent event{ + .session_id = session.session_id, + .event_bus = session.event_bus, + .app_launch_cmd = session.app.run_cmd, + }; + session.event_bus->fire_event(immer::box(event)); + return ok_msg(req.seq_number); } @@ -187,7 +195,7 @@ RTSP_PACKET message_handler(const RTSP_PACKET &req, const state::StreamSession & case utils::hash("ANNOUNCE"): return announce(req, session); case utils::hash("PLAY"): - return play(req); + return play(req, session); default: logs::log(logs::warning, "[RTSP] command {} not found", cmd); return error_msg(404, "NOT FOUND", req.seq_number); diff --git a/src/wolf/rtsp/net.hpp b/src/wolf/rtsp/net.hpp index 966b627c..04a3776e 100644 --- a/src/wolf/rtsp/net.hpp +++ b/src/wolf/rtsp/net.hpp @@ -200,33 +200,27 @@ class tcp_server { * * @return the thread instance */ -std::thread start_server(int port, immer::box state) { - auto thread = std::thread( - [port](immer::box state) { - try { - boost::asio::io_context io_context; - tcp_server server(io_context, port, state); - - logs::log(logs::info, "RTSP server started on port: {}", port); - - auto stop_handler = state->event_bus->register_handler>( - [sess_id = state->session_id, &io_context](immer::box term_ev) { - if (term_ev->session_id == sess_id) { - logs::log(logs::info, "RTSP received termination, stopping."); - io_context.stop(); - } - }); - - // This will block here until the context is stopped - io_context.run(); - stop_handler.unregister(); - } catch (std::exception &e) { - logs::log(logs::error, "Unable to create RTSP server on port: {} ex: {}", port, e.what()); - } - }, - std::move(state)); - - return thread; +void run_server(int port, immer::box state) { + try { + boost::asio::io_context io_context; + tcp_server server(io_context, port, state); + + logs::log(logs::info, "RTSP server started on port: {}", port); + + auto stop_handler = state->event_bus->register_handler>( + [sess_id = state->session_id, &io_context](immer::box term_ev) { + if (term_ev->session_id == sess_id) { + logs::log(logs::info, "RTSP received termination, stopping."); + io_context.stop(); + } + }); + + // This will block here until the context is stopped + io_context.run(); + stop_handler.unregister(); + } catch (std::exception &e) { + logs::log(logs::error, "Unable to create RTSP server on port: {} ex: {}", port, e.what()); + } } } // namespace rtsp diff --git a/src/wolf/state/configTOML.cpp b/src/wolf/state/configTOML.cpp index d433b877..b551102d 100644 --- a/src/wolf/state/configTOML.cpp +++ b/src/wolf/state/configTOML.cpp @@ -265,6 +265,7 @@ template <> struct into { return toml::value{ {"title", f.base.title}, {"support_hdr", f.base.support_hdr}, + {"run_cmd", f.run_cmd} // TODO: [video] [audio] are they needed? }; } diff --git a/src/wolf/wolf.cpp b/src/wolf/wolf.cpp index 90dfada0..818b5b68 100644 --- a/src/wolf/wolf.cpp +++ b/src/wolf/wolf.cpp @@ -1,9 +1,11 @@ +#include #include #include #include #include #include #include +#include #include #include #include @@ -14,6 +16,8 @@ #include #include +namespace ba = boost::asio; + /** * @brief Will try to load the config file and fallback to defaults */ @@ -114,13 +118,9 @@ void user_pin_handler(state::PairSignal pair_request) { } /** - * A bit tricky here: on a basic level we want a map of [session_id] -> std::thread - * - * Unfortunately std::thread doesn't have a copy constructor AND `.join()` is not marked as `const` - * So we wrap the thread with a immer box in order to be able to store it in the vector (copy constructor) - * AND we wrap it in a unique_ptr so that we can access it without `const` (.join()) + * A bit tricky here: on a basic level we want a map of [session_id] -> thread_pool */ -typedef immer::atom>>>> TreadsMapAtom; +typedef immer::atom>> TreadsMapAtom; /** * Glue code in order to start the sessions in response to events fired in the bus @@ -131,102 +131,100 @@ typedef immer::atom &event_bus, TreadsMapAtom &threads) { + immer::vector_transient> handlers; + // HTTP PIN - auto pair_sig = event_bus->register_handler(&user_pin_handler); + handlers.push_back(event_bus->register_handler(&user_pin_handler)); // RTSP - auto rtsp_launch_sig = event_bus->register_handler>( + handlers.push_back(event_bus->register_handler>( [&threads](immer::box stream_session) { + // Create pool + auto t_pool = std::make_shared(10); + // Start RTSP - auto rtsp_thread = rtsp::start_server(stream_session->rtsp_port, stream_session); - auto rtsp_thread_ptr = std::make_unique(std::move(rtsp_thread)); - - // Start app process - auto process_thread = process::spawn_process(process::START_PROCESS_EV{.session_id = stream_session->session_id, - .event_bus = stream_session->event_bus, - .run_cmd = stream_session->app.run_cmd}); - auto process_thread_ptr = std::make_unique(std::move(process_thread)); - - threads.update([&rtsp_thread_ptr, &process_thread_ptr, sess_id = stream_session->session_id](auto t_map) { - return t_map.update(sess_id, [&rtsp_thread_ptr, &process_thread_ptr](auto t_vec) { - // TODO: use transient instead - return t_vec.push_back(std::move(rtsp_thread_ptr)).push_back(std::move(process_thread_ptr)); - }); + ba::post(*t_pool, [stream_session]() { rtsp::run_server(stream_session->rtsp_port, stream_session); }); + + // Store pool for others + threads.update([sess_id = stream_session->session_id, t_pool](auto t_map) { + return t_map.update(sess_id, [t_pool](auto box) { return t_pool; }); }); - }); + })); // Control thread control::init(); // Need to initialise enet once - auto ctrl_launch_sig = event_bus->register_handler>( + handlers.push_back(event_bus->register_handler>( [&threads](immer::box control_sess) { - auto thread = control::start_service(control_sess); - auto thread_ptr = std::make_unique(std::move(thread)); + auto t_pool = threads.load()->at(control_sess->session_id); - threads.update([&thread_ptr, sess_id = control_sess->session_id](auto t_map) { - return t_map.update(sess_id, [&thread_ptr](auto t_vec) { return t_vec.push_back(std::move(thread_ptr)); }); + // Start control stream + ba::post(*t_pool, [control_sess]() { + int timeout_millis = 1000; // TODO: config? + control::run_control(control_sess, timeout_millis); }); - }); + })); + + handlers.push_back(event_bus->register_handler>( + [&threads](immer::box launch_ev) { + auto t_pool = threads.load()->at(launch_ev->session_id); + + // Setup inputs and start selected app + ba::post(*t_pool, [launch_ev]() { + // create virtual inputs + auto input_setup = input::setup_handlers(launch_ev->session_id, launch_ev->event_bus); + + // Start app + process::run_process(launch_ev); + + // when the app exits, cleanup + for (const auto &handler : input_setup.registered_handlers) { + handler->unregister(); + } + }); + })); // GStreamer video streaming::init(); // Need to initialise streaming once - auto video_launch_sig = event_bus->register_handler>( + handlers.push_back(event_bus->register_handler>( [&threads](immer::box video_sess) { - auto thread = std::thread( - [](auto video_sess) { - auto client_port = rtp::wait_for_ping(video_sess->port); - streaming::start_streaming_video(std::move(video_sess), client_port); - }, - video_sess); - auto thread_ptr = std::make_unique(std::move(thread)); - - threads.update([&thread_ptr, sess_id = video_sess->session_id](auto t_map) { - return t_map.update(sess_id, [&thread_ptr](auto t_vec) { return t_vec.push_back(std::move(thread_ptr)); }); + auto t_pool = threads.load()->at(video_sess->session_id); + + ba::post(*t_pool, [video_sess]() { + auto client_port = rtp::wait_for_ping(video_sess->port); + streaming::start_streaming_video(std::move(video_sess), client_port); }); - }); + })); // GStreamer audio - auto audio_launch_sig = event_bus->register_handler>( + handlers.push_back(event_bus->register_handler>( [&threads](immer::box audio_sess) { - auto thread = std::thread( - [](auto audio_sess) { - auto client_port = rtp::wait_for_ping(audio_sess->port); - streaming::start_streaming_audio(std::move(audio_sess), client_port); - }, - audio_sess); - auto thread_ptr = std::make_unique(std::move(thread)); - - threads.update([&thread_ptr, sess_id = audio_sess->session_id](auto t_map) { - return t_map.update(sess_id, [&thread_ptr](auto t_vec) { return t_vec.push_back(std::move(thread_ptr)); }); + auto t_pool = threads.load()->at(audio_sess->session_id); + + ba::post(*t_pool, [audio_sess]() { + auto client_port = rtp::wait_for_ping(audio_sess->port); + streaming::start_streaming_audio(std::move(audio_sess), client_port); }); - }); + })); // On control session end, let's wait for all threads to finish then clean them up - auto ctrl_handler = event_bus->register_handler>( + handlers.push_back(event_bus->register_handler>( [&threads](immer::box event) { // Events are dispatched from the calling thread; in this case it'll be the control stream thread. // We have to create a new thread to process the cleaning and detach it from the original thread auto cleanup_thread = std::thread([&threads, sess_id = event->session_id]() { - auto t_vec = threads.load()->at(sess_id); + auto t_pool = threads.load()->at(sess_id); + logs::log(logs::debug, "Terminated session: {}, waiting for thread_pool to finish", sess_id); - logs::log(logs::debug, "Terminated session: {}, waiting for {} threads to finish", sess_id, t_vec.size()); + t_pool->wait(); - for (const auto &t_ptr : t_vec) { - auto thread = t_ptr->get(); - thread->join(); // Wait for the thread to be over - } - - logs::log(logs::debug, "Removing session: {}", sess_id); + logs::log(logs::info, "Closed session: {}", sess_id); threads.update([sess_id](auto t_map) { return t_map.erase(sess_id); }); }); + cleanup_thread.detach(); - }); - - return immer::array>{std::move(pair_sig), - std::move(rtsp_launch_sig), - std::move(ctrl_launch_sig), - std::move(video_launch_sig), - std::move(audio_launch_sig), - std::move(ctrl_handler)}; + })); + + return handlers.persistent(); } /** From 87bc26f3f4e9aa8e001fc164a9b6b5be43e0e4ee Mon Sep 17 00:00:00 2001 From: ABeltramo Date: Sun, 15 Jan 2023 13:34:58 +0000 Subject: [PATCH 08/25] feat: moved input from std::thread to thread pool --- src/input/input/input.hpp | 5 ++++- src/input/platforms/linux/uinput.cpp | 12 ++++++------ src/input/platforms/unknown/input.cpp | 4 +++- src/wolf/wolf.cpp | 4 ++-- 4 files changed, 15 insertions(+), 10 deletions(-) diff --git a/src/input/input/input.hpp b/src/input/input/input.hpp index b73767d0..576b4beb 100644 --- a/src/input/input/input.hpp +++ b/src/input/input/input.hpp @@ -1,6 +1,7 @@ #pragma once #include "moonlight/data-structures.hpp" +#include #include #include #include @@ -19,7 +20,9 @@ struct InputReady { * PLATFORM DEPENDENT * will wait for events on the event bus and setup virtual devices accordingly. */ -InputReady setup_handlers(std::size_t session_id, const std::shared_ptr &event_bus); +InputReady setup_handlers(std::size_t session_id, + const std::shared_ptr &event_bus, + std::shared_ptr t_pool); /** * A packet of type INPUT_DATA will have different shapes based on the type diff --git a/src/input/platforms/linux/uinput.cpp b/src/input/platforms/linux/uinput.cpp index d79f5042..a58905bd 100644 --- a/src/input/platforms/linux/uinput.cpp +++ b/src/input/platforms/linux/uinput.cpp @@ -18,7 +18,6 @@ #include #include #include -#include namespace input { @@ -383,7 +382,9 @@ void controller_handle(libevdev_uinput *controller, } // namespace controller -InputReady setup_handlers(std::size_t session_id, const std::shared_ptr &event_bus) { +InputReady setup_handlers(std::size_t session_id, + const std::shared_ptr &event_bus, + std::shared_ptr t_pool) { logs::log(logs::debug, "Setting up input handlers for session: {}", session_id); auto v_devices = std::make_shared(); @@ -504,18 +505,17 @@ InputReady setup_handlers(std::size_t session_id, const std::shared_ptr>(false); - auto keyboard_thread = std::make_shared([v_devices, keyboard_state, kb_thread_over]() { + boost::asio::post(*t_pool, ([v_devices, keyboard_state, kb_thread_over]() { while (!kb_thread_over->load()) { std::this_thread::sleep_for(50ms); // TODO: should this be configurable? keyboard::keyboard_repeat_press(v_devices->keyboard->get(), keyboard_state->load()); } - }); + })); auto end_handler = event_bus->register_handler>( - [sess_id = session_id, kb_thread_over, keyboard_thread](immer::box event) { + [sess_id = session_id, kb_thread_over](immer::box event) { if (event->session_id == sess_id) { kb_thread_over->update([](bool terminate) { return true; }); - keyboard_thread->join(); } }); diff --git a/src/input/platforms/unknown/input.cpp b/src/input/platforms/unknown/input.cpp index a3bd7152..f86effa2 100644 --- a/src/input/platforms/unknown/input.cpp +++ b/src/input/platforms/unknown/input.cpp @@ -3,7 +3,9 @@ namespace input { -InputReady setup_handlers(std::size_t session_id, const std::shared_ptr &event_bus) { +InputReady setup_handlers(std::size_t session_id, + const std::shared_ptr &event_bus, + std::shared_ptr t_pool) { logs::log(logs::error, "Unable to setup input handlers for the current platform."); return {.devices_paths = {}, .registered_handlers = {}}; } diff --git a/src/wolf/wolf.cpp b/src/wolf/wolf.cpp index 818b5b68..f7cf7507 100644 --- a/src/wolf/wolf.cpp +++ b/src/wolf/wolf.cpp @@ -169,9 +169,9 @@ auto setup_sessions_handlers(std::shared_ptr &event_bus, TreadsMa auto t_pool = threads.load()->at(launch_ev->session_id); // Setup inputs and start selected app - ba::post(*t_pool, [launch_ev]() { + ba::post(*t_pool, [launch_ev, t_pool]() { // create virtual inputs - auto input_setup = input::setup_handlers(launch_ev->session_id, launch_ev->event_bus); + auto input_setup = input::setup_handlers(launch_ev->session_id, launch_ev->event_bus, t_pool); // Start app process::run_process(launch_ev); From 99e12cc37b6ea858f54877e447b4f6e6bc0a7be0 Mon Sep 17 00:00:00 2001 From: ABeltramo Date: Sun, 15 Jan 2023 13:48:07 +0000 Subject: [PATCH 09/25] feat: early termination of process will close the connection --- src/wolf/control/control.cpp | 17 +++++++++++++---- src/wolf/process/process.cpp | 5 ++--- src/wolf/process/process.hpp | 4 ++++ 3 files changed, 19 insertions(+), 7 deletions(-) diff --git a/src/wolf/control/control.cpp b/src/wolf/control/control.cpp index 6460091f..199c0924 100644 --- a/src/wolf/control/control.cpp +++ b/src/wolf/control/control.cpp @@ -2,6 +2,7 @@ #include #include #include +#include #include namespace control { @@ -74,8 +75,16 @@ void run_control(immer::box control_sess, int timeout_mil logs::log(logs::info, "Control server started on port: {}", control_sess->port); ENetEvent event; - bool terminated = false; - while (!terminated && enet_host_service(host.get(), &event, timeout_millis) > 0) { + immer::atom terminated(false); + + auto app_stop_handler = control_sess->event_bus->register_handler( + [&terminated, sess_id = control_sess->session_id](process::AppStoppedEvent ev) { + if (ev.session_id == sess_id) { + terminated.store(true); // TODO: is there a way to better signal this to Moonlight? + } + }); + + while (!terminated.load() && enet_host_service(host.get(), &event, timeout_millis) > 0) { auto [client_ip, client_port] = get_ip((sockaddr *)&event.peer->address.address); switch (event.type) { @@ -86,7 +95,7 @@ void run_control(immer::box control_sess, int timeout_mil break; case ENET_EVENT_TYPE_DISCONNECT: logs::log(logs::debug, "[ENET] disconnected client: {}:{}", client_ip, client_port); - terminated = true; + terminated.store(true); break; case ENET_EVENT_TYPE_RECEIVE: enet_packet packet = {event.packet, enet_packet_destroy}; @@ -125,9 +134,9 @@ void run_control(immer::box control_sess, int timeout_mil break; } } - // Failsafe, when we get out of the loop we have to signal to terminate the session logs::log(logs::debug, "[ENET] terminating session: {}", control_sess->session_id); + app_stop_handler.unregister(); auto terminate_ev = TerminateEvent{control_sess->session_id}; control_sess->event_bus->fire_event(immer::box{terminate_ev}); diff --git a/src/wolf/process/process.cpp b/src/wolf/process/process.cpp index f6e54f85..eae7102e 100644 --- a/src/wolf/process/process.cpp +++ b/src/wolf/process/process.cpp @@ -43,9 +43,8 @@ void run_process(immer::box process_ev) { ios.run(); // This will stop here until the process is over if (*client_connected.load()) { - // TODO: the process terminated in error before the user closed the connection - // We should signal this to the control event so that it can close the connection downstream - // fire_event( ... ) + logs::log(logs::warning, "[PROCESS] Process terminated before the user closed the connection."); + process_ev->event_bus->fire_event(AppStoppedEvent{.session_id = process_ev->session_id}); } child_proc.wait(); // to avoid a zombie process & get the exit code diff --git a/src/wolf/process/process.hpp b/src/wolf/process/process.hpp index bfb04911..b5b34c97 100644 --- a/src/wolf/process/process.hpp +++ b/src/wolf/process/process.hpp @@ -21,6 +21,10 @@ struct LaunchAPPEvent { std::string app_launch_cmd; }; +struct AppStoppedEvent{ + std::size_t session_id; +}; + namespace bp = boost::process; void run_process(immer::box process_ev); From 6f6b562bd2711f648cc724ffdd6000c30659956c Mon Sep 17 00:00:00 2001 From: ABeltramo Date: Sun, 15 Jan 2023 20:28:23 +0000 Subject: [PATCH 10/25] feat: basic Github Action in order to build docker images --- .github/workflows/docker-build.yml | 98 ++++++++++++++++++++++++++++++ 1 file changed, 98 insertions(+) diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index e69de29b..faa83cc8 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -0,0 +1,98 @@ +name: docker build + +on: + push: + +jobs: + buildx: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + + # Set derived configuration variables: + # - images: images to build (docker and/or github) + # - push: if images need to be uploaded to repository (when secrets available) + # - has_docker_token + # - has_github_token + # - cache_from: image tag to use for imported cache + # - cache_to: image tag to use for exported cache + # - github_server_url: reference to source code repository + - name: Prepare + id: prep + run: | + IMAGES="" + PUSH=false + if [ -n "${{ secrets.DOCKERHUB_TOKEN }}" ]; then + IMAGES="gameonwhales/wolf" + PUSH=true + echo ::set-output name=has_docker_token::true + fi + if [ -n "${{ secrets.GHCR_TOKEN }}" ]; then + REGISTRY_IMAGE="ghcr.io/${{ github.repository_owner }}/wolf" + if [ "$IMAGES" = "" ]; then + IMAGES="ghcr.io/${REGISTRY_IMAGE}" + else + IMAGES="$IMAGES,ghcr.io/${REGISTRY_IMAGE}" + fi + PUSH=true + echo ::set-output name=has_github_token::true + echo ::set-output name=cache_from::"type=registry,ref=${REGISTRY_IMAGE}:buildcache" + echo ::set-output name=cache_to::"type=registry,ref=${REGISTRY_IMAGE}:buildcache,mode=max" + else + echo ::set-output name=cache_from::"type=registry,ref=${REGISTRY_IMAGE}:buildcache" + echo ::set-output name=cache_to::"" + fi + echo ::set-output name=images::"${IMAGES}" + echo ::set-output name=push::${PUSH} + echo ::set-output name=github_server_url::"${GITHUB_SERVER_URL}" + + - name: Docker meta + id: meta + uses: docker/metadata-action@v4 + with: + # list of Docker images to use as base name for tags + images: ${{ steps.prep.outputs.images }} + # generate Docker tags based on the following events/attributes + tags: | + type=semver,pattern={{version}} + type=semver,pattern={{major}} + type=edge,branch=master + type=ref,event=branch + type=sha + flavor: latest=auto #latest will point to last semver version (stable) + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + with: + install: true + version: latest + driver-opts: image=moby/buildkit:master + + - name: Login to DockerHub + if: steps.prep.outputs.has_docker_token != '' # secrets not available in PRs + uses: docker/login-action@v2 + with: + username: abeltramo + password: ${{ secrets.DOCKERHUB_TOKEN }} + + + - name: Login to GitHub Container Registry + if: steps.prep.outputs.has_github_token != '' # secrets not available in PRs + uses: docker/login-action@v2 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GHCR_TOKEN }} + + - name: Build and Push + id: docker_build + uses: docker/build-push-action@v3 + with: + builder: ${{ steps.buildx.outputs.name }} + push: ${{ steps.prep.outputs.push }} + tags: ${{ steps.meta.outputs.tags }} + build-args: | + IMAGE_SOURCE=${{ steps.prep.outputs.github_server_url }}/${{ github.repository }} + cache-from: ${{ steps.prep.outputs.cache_from }} + cache-to: ${{ steps.prep.outputs.cache_to }} \ No newline at end of file From 6fbca1a018a55a0d0083e05ef10bf68931839f82 Mon Sep 17 00:00:00 2001 From: ABeltramo Date: Sun, 15 Jan 2023 21:00:35 +0000 Subject: [PATCH 11/25] fix: GA - upgrade to environment files --- .github/workflows/docker-build.yml | 20 ++++++++++---------- .github/workflows/linux-build-test.yml | 2 -- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index faa83cc8..1f0afea8 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -26,7 +26,7 @@ jobs: if [ -n "${{ secrets.DOCKERHUB_TOKEN }}" ]; then IMAGES="gameonwhales/wolf" PUSH=true - echo ::set-output name=has_docker_token::true + echo "has_docker_token=true" >> $GITHUB_OUTPUT fi if [ -n "${{ secrets.GHCR_TOKEN }}" ]; then REGISTRY_IMAGE="ghcr.io/${{ github.repository_owner }}/wolf" @@ -36,16 +36,16 @@ jobs: IMAGES="$IMAGES,ghcr.io/${REGISTRY_IMAGE}" fi PUSH=true - echo ::set-output name=has_github_token::true - echo ::set-output name=cache_from::"type=registry,ref=${REGISTRY_IMAGE}:buildcache" - echo ::set-output name=cache_to::"type=registry,ref=${REGISTRY_IMAGE}:buildcache,mode=max" + echo "has_github_token=true" >> $GITHUB_OUTPUT + echo "cache_from=type=registry,ref=${REGISTRY_IMAGE}:buildcache" >> $GITHUB_OUTPUT + echo "cache_to=type=registry,ref=${REGISTRY_IMAGE}:buildcache,mode=max" >> $GITHUB_OUTPUT else - echo ::set-output name=cache_from::"type=registry,ref=${REGISTRY_IMAGE}:buildcache" - echo ::set-output name=cache_to::"" + echo "cache_from=type=registry,ref=${REGISTRY_IMAGE}:buildcache" >> $GITHUB_OUTPUT + echo "cache_to=" >> $GITHUB_OUTPUT fi - echo ::set-output name=images::"${IMAGES}" - echo ::set-output name=push::${PUSH} - echo ::set-output name=github_server_url::"${GITHUB_SERVER_URL}" + echo "images=${IMAGES}" >> $GITHUB_OUTPUT + echo "push=${PUSH}" >> $GITHUB_OUTPUT + echo "github_server_url=${GITHUB_SERVER_URL}" >> $GITHUB_OUTPUT - name: Docker meta id: meta @@ -60,7 +60,7 @@ jobs: type=edge,branch=master type=ref,event=branch type=sha - flavor: latest=auto #latest will point to last semver version (stable) + flavor: latest=false # let's not produce a :latest tag - name: Set up Docker Buildx uses: docker/setup-buildx-action@v2 diff --git a/.github/workflows/linux-build-test.yml b/.github/workflows/linux-build-test.yml index 314f9ef0..68fabfa5 100644 --- a/.github/workflows/linux-build-test.yml +++ b/.github/workflows/linux-build-test.yml @@ -30,8 +30,6 @@ jobs: steps: - uses: actions/checkout@v3 - with: - submodules: recursive - name: Prepare environment # ubuntu-latest breaks without libunwind-dev, From 0af1cbd1b5f41a71ed92120e45369b1eafe77e20 Mon Sep 17 00:00:00 2001 From: ABeltramo Date: Sun, 15 Jan 2023 21:30:35 +0000 Subject: [PATCH 12/25] feat: Github actions, dependabot, codeberg mirror --- .github/dependabot.yml | 22 ++++++++++++++++++++++ .github/workflows/docker-build.yml | 9 ++++++++- .github/workflows/linux-build-test.yml | 1 - .github/workflows/mirror-repo.yml | 14 ++++++++++++++ 4 files changed, 44 insertions(+), 2 deletions(-) create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/mirror-repo.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..04f863c9 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,22 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates + +version: 2 +updates: + - package-ecosystem: "docker" + directory: "/" + schedule: + interval: "daily" + time: "00:00" + target-branch: "nightly" + open-pull-requests-limit: 10 + + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "daily" + time: "00:00" + target-branch: "nightly" + open-pull-requests-limit: 10 diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index 1f0afea8..963278a1 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -1,10 +1,17 @@ -name: docker build +name: Docker build on: push: + # TODO: enable workflow_run when merged into master +# workflow_run: +# workflows: [ "Linux build and test" ] +# types: +# - completed jobs: buildx: + # Only run this when tests are passing, see: https://github.com/orgs/community/discussions/26238 + # if: ${{ github.event.workflow_run.conclusion == 'success' }} runs-on: ubuntu-latest steps: - name: Checkout diff --git a/.github/workflows/linux-build-test.yml b/.github/workflows/linux-build-test.yml index 68fabfa5..a630ff9a 100644 --- a/.github/workflows/linux-build-test.yml +++ b/.github/workflows/linux-build-test.yml @@ -5,7 +5,6 @@ on: [ push, pull_request ] jobs: build: - name: ${{matrix.cxx}}, C++${{matrix.std}}, ${{matrix.build_type}} runs-on: ubuntu-22.04 strategy: fail-fast: false diff --git a/.github/workflows/mirror-repo.yml b/.github/workflows/mirror-repo.yml new file mode 100644 index 00000000..d2e8b987 --- /dev/null +++ b/.github/workflows/mirror-repo.yml @@ -0,0 +1,14 @@ +# Pushes the contents of the repo to the Codeberg mirror +name: Repo Mirror +on: [ push ] +jobs: + codeberg: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + - uses: pixta-dev/repository-mirroring-action@v1 + with: + target_repo_url: "git@codeberg.org:games-on-whales/wolf.git" + ssh_private_key: ${{ secrets.CODEBERG_SSH }} \ No newline at end of file From 458a2727bc9ad2b612c2eac8e82f430d79f9c263 Mon Sep 17 00:00:00 2001 From: ABeltramo Date: Fri, 20 Jan 2023 19:11:02 +0000 Subject: [PATCH 13/25] fix: uinput mouse fix --- docs/modules/protocols/pages/input-data.adoc | 9 +++++- docs/modules/user/pages/quickstart.adoc | 29 ++++++++++++++++-- src/input/input/input.hpp | 3 +- src/input/platforms/linux/uinput.cpp | 32 +++++++++----------- src/input/platforms/linux/uinput.hpp | 6 ++-- tests/platforms/linux/input.cpp | 16 +++++----- 6 files changed, 63 insertions(+), 32 deletions(-) diff --git a/docs/modules/protocols/pages/input-data.adoc b/docs/modules/protocols/pages/input-data.adoc index 4ce5b2d2..fc66716b 100644 --- a/docs/modules/protocols/pages/input-data.adoc +++ b/docs/modules/protocols/pages/input-data.adoc @@ -29,6 +29,8 @@ The first 4 bytes of a packet of type `INPUT_DATA` will determine the type of in == Mouse: relative move +Moonlight will send mouse relative coordinates when the option "Optimize mouse for remote desktop instead of games" is turned `OFF`. + .The full format of a `MOUSE_MOVE_REL` packet [packetdiag,format=svg,align="center"] .... @@ -47,8 +49,13 @@ The first 4 bytes of a packet of type `INPUT_DATA` will determine the type of in `Delta X` and `Delta Y` defines the relative movement that the mouse must perform. +[NOTE] +`Delta X` and `Delta Y` are encoded as https://en.wikipedia.org/wiki/Endianness[big endian]. + == Mouse: absolute move +Moonlight will send mouse absolute coordinates when the option "Optimize mouse for remote desktop instead of games" is turned `ON`. + .The full format of a `MOUSE_MOVE_ABS` packet [packetdiag,format=svg,align="center"] .... @@ -74,7 +81,7 @@ In order to define an absolute position Moonlight will send both: * `X`,`Y` are the absolute coordinates of the mouse on the client side [NOTE] -`X`, `Y`, `width` and `height` are all sent in big endian. +`X`, `Y`, `width` and `height` are encoded as https://en.wikipedia.org/wiki/Endianness[big endian]. == Mouse: button diff --git a/docs/modules/user/pages/quickstart.adoc b/docs/modules/user/pages/quickstart.adoc index 2fba9a3a..35196846 100644 --- a/docs/modules/user/pages/quickstart.adoc +++ b/docs/modules/user/pages/quickstart.adoc @@ -12,7 +12,7 @@ The only way to run Wolf right now is to manually compile it. .Build dependencies [source,bash] .... -apt install -y ninja-build clang libboost-thread-dev libboost-filesystem-dev libboost-log-dev libboost-stacktrace-dev libssl-dev libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libevdev-dev +apt install -y ninja-build cmake clang libboost-thread-dev libboost-filesystem-dev libboost-log-dev libboost-stacktrace-dev libssl-dev libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libevdev-dev .... .Compile @@ -44,4 +44,29 @@ INFO | HTTPS server listening on port: 47984 You should now be able to point Moonlight to the IP address of the server and start the pairing process. -TIP: Make sure to follow the next steps on how to configure Wolf! \ No newline at end of file +TIP: Make sure to follow the next steps on how to configure Wolf! + +== devices support + +We use uinput to create virtual devices (Mouse, Keyboard and Joypad), make sure that `/dev/uinput` is present in the host: + +[source, bash] +.... +ls -la /dev/uinput +crw------- 1 root root 10, 223 Jan 17 09:08 /dev/uinput +.... + +.Add your user to group `input` +[source, bash] +.... +sudo usermod -a -G input $USER +.... + +.Create `udev` rules +[source, bash] +.... +echo 'KERNEL=="uinput", GROUP="input", MODE="0660", OPTIONS+="static_node=uinput"' | \ +sudo tee /etc/udev/rules.d/85-wolf-virtual-inputs.rules +.... + +Reboot. \ No newline at end of file diff --git a/src/input/input/input.hpp b/src/input/input/input.hpp index 576b4beb..76db2a36 100644 --- a/src/input/input/input.hpp +++ b/src/input/input/input.hpp @@ -86,7 +86,8 @@ struct MOUSE_MOVE_ABS_PACKET : INPUT_PKT { struct MOUSE_BUTTON_PACKET : INPUT_PKT { char action; - int button; + short zero; + short button; }; struct MOUSE_SCROLL_PACKET : INPUT_PKT { diff --git a/src/input/platforms/linux/uinput.cpp b/src/input/platforms/linux/uinput.cpp index a58905bd..be2cffe1 100644 --- a/src/input/platforms/linux/uinput.cpp +++ b/src/input/platforms/linux/uinput.cpp @@ -72,7 +72,7 @@ std::optional create_mouse(libevdev *dev) { return uidev; } -std::optional create_touchpad(libevdev *dev) { +std::optional create_mouse_abs(libevdev *dev) { libevdev_uinput *uidev; libevdev_set_uniq(dev, "Wolf Touchpad"); @@ -84,12 +84,10 @@ std::optional create_touchpad(libevdev *dev) { libevdev_enable_property(dev, INPUT_PROP_DIRECT); libevdev_enable_event_type(dev, EV_KEY); - libevdev_enable_event_code(dev, EV_KEY, BTN_TOUCH, nullptr); - libevdev_enable_event_code(dev, EV_KEY, BTN_TOOL_PEN, nullptr); - libevdev_enable_event_code(dev, EV_KEY, BTN_TOOL_FINGER, nullptr); + libevdev_enable_event_code(dev, EV_KEY, BTN_LEFT, nullptr); struct input_absinfo absinfo { - .value = 0, .minimum = 0, .maximum = 0, .fuzz = 1, .flat = 0, .resolution = 40 + .value = 0, .minimum = 0, .maximum = 65535, .fuzz = 1, .flat = 0, .resolution = 28 }; libevdev_enable_event_type(dev, EV_ABS); @@ -110,18 +108,20 @@ std::optional create_touchpad(libevdev *dev) { } void move_mouse(libevdev_uinput *mouse, const data::MOUSE_MOVE_REL_PACKET &move_pkt) { - if (move_pkt.delta_x) { - libevdev_uinput_write_event(mouse, EV_REL, REL_X, move_pkt.delta_x); + short delta_x = boost::endian::big_to_native(move_pkt.delta_x); + short delta_y = boost::endian::big_to_native(move_pkt.delta_y); + if (delta_x) { + libevdev_uinput_write_event(mouse, EV_REL, REL_X, delta_x); } - if (move_pkt.delta_y) { - libevdev_uinput_write_event(mouse, EV_REL, REL_Y, move_pkt.delta_y); + if (delta_y) { + libevdev_uinput_write_event(mouse, EV_REL, REL_Y, delta_y); } libevdev_uinput_write_event(mouse, EV_SYN, SYN_REPORT, 0); } -void move_touchpad(libevdev_uinput *mouse, const data::MOUSE_MOVE_ABS_PACKET &move_pkt) { +void move_mouse_abs(libevdev_uinput *mouse, const data::MOUSE_MOVE_ABS_PACKET &move_pkt) { float x = boost::endian::big_to_native(move_pkt.x); float y = boost::endian::big_to_native(move_pkt.y); float width = boost::endian::big_to_native(move_pkt.width); @@ -132,8 +132,6 @@ void move_touchpad(libevdev_uinput *mouse, const data::MOUSE_MOVE_ABS_PACKET &mo libevdev_uinput_write_event(mouse, EV_ABS, ABS_X, scaled_x); libevdev_uinput_write_event(mouse, EV_ABS, ABS_Y, scaled_y); - libevdev_uinput_write_event(mouse, EV_KEY, BTN_TOOL_FINGER, 1); - libevdev_uinput_write_event(mouse, EV_KEY, BTN_TOOL_FINGER, 0); libevdev_uinput_write_event(mouse, EV_SYN, SYN_REPORT, 0); } @@ -396,9 +394,9 @@ InputReady setup_handlers(std::size_t session_id, devices_paths.push_back(libevdev_uinput_get_devnode(*mouse_el)); } - libevdev_ptr touch_dev(libevdev_new(), ::libevdev_free); - if (auto touch_el = mouse::create_touchpad(touch_dev.get())) { - v_devices->touchpad = {*touch_el, ::libevdev_uinput_destroy}; + libevdev_ptr mouse_abs_dev(libevdev_new(), ::libevdev_free); + if (auto touch_el = mouse::create_mouse_abs(mouse_abs_dev.get())) { + v_devices->mouse_abs = {*touch_el, ::libevdev_uinput_destroy}; devices_paths.push_back(libevdev_uinput_get_devnode(*touch_el)); } @@ -432,8 +430,8 @@ InputReady setup_handlers(std::size_t session_id, break; case data::MOUSE_MOVE_ABS: logs::log(logs::trace, "[INPUT] Received input of type: MOUSE_MOVE_ABS"); - if (v_devices->touchpad) { - mouse::move_touchpad(v_devices->touchpad->get(), *(data::MOUSE_MOVE_ABS_PACKET *)input); + if (v_devices->mouse_abs) { + mouse::move_mouse_abs(v_devices->mouse_abs->get(), *(data::MOUSE_MOVE_ABS_PACKET *)input); } break; case data::MOUSE_BUTTON: diff --git a/src/input/platforms/linux/uinput.hpp b/src/input/platforms/linux/uinput.hpp index 48aacd3a..78b0120f 100644 --- a/src/input/platforms/linux/uinput.hpp +++ b/src/input/platforms/linux/uinput.hpp @@ -12,7 +12,7 @@ using libevdev_uinput_ptr = std::shared_ptr; struct VirtualDevices { std::optional mouse; - std::optional touchpad; + std::optional mouse_abs; std::optional keyboard; immer::array controllers{}; @@ -22,11 +22,11 @@ namespace mouse { std::optional create_mouse(libevdev *dev); -std::optional create_touchpad(libevdev *dev); +std::optional create_mouse_abs(libevdev *dev); void move_mouse(libevdev_uinput *mouse, const data::MOUSE_MOVE_REL_PACKET &move_pkt); -void move_touchpad(libevdev_uinput *mouse, const data::MOUSE_MOVE_ABS_PACKET &move_pkt); +void move_mouse_abs(libevdev_uinput *mouse, const data::MOUSE_MOVE_ABS_PACKET &move_pkt); void mouse_press(libevdev_uinput *mouse, const data::MOUSE_BUTTON_PACKET &btn_pkt); diff --git a/tests/platforms/linux/input.cpp b/tests/platforms/linux/input.cpp index 7b17d5b1..2cd56efc 100644 --- a/tests/platforms/linux/input.cpp +++ b/tests/platforms/linux/input.cpp @@ -73,13 +73,13 @@ TEST_CASE("uinput - mouse", "UINPUT") { REQUIRE(rc == LIBEVDEV_READ_STATUS_SUCCESS); REQUIRE_THAT(libevdev_event_type_get_name(ev.type), Equals("EV_REL")); REQUIRE_THAT(libevdev_event_code_get_name(ev.type, ev.code), Equals("REL_X")); - REQUIRE(ev.value == mv_packet.delta_x); + REQUIRE(10 == mv_packet.delta_x); rc = libevdev_next_event(mouse_dev.get(), LIBEVDEV_READ_FLAG_NORMAL, &ev); REQUIRE(rc == LIBEVDEV_READ_STATUS_SUCCESS); REQUIRE_THAT(libevdev_event_type_get_name(ev.type), Equals("EV_REL")); REQUIRE_THAT(libevdev_event_code_get_name(ev.type, ev.code), Equals("REL_Y")); - REQUIRE(ev.value == mv_packet.delta_y); + REQUIRE(20 == mv_packet.delta_y); rc = libevdev_next_event(mouse_dev.get(), LIBEVDEV_READ_FLAG_NORMAL, &ev); REQUIRE(rc == LIBEVDEV_READ_STATUS_SUCCESS); @@ -122,22 +122,22 @@ TEST_CASE("uinput - mouse", "UINPUT") { } TEST_CASE("uinput - touchpad", "UINPUT") { - libevdev_ptr touch_dev(libevdev_new(), ::libevdev_free); - libevdev_uinput_ptr touch_el = {mouse::create_touchpad(touch_dev.get()).value(), ::libevdev_uinput_destroy}; + libevdev_ptr mouse_abs(libevdev_new(), ::libevdev_free); + libevdev_uinput_ptr touch_el = {mouse::create_mouse_abs(mouse_abs.get()).value(), ::libevdev_uinput_destroy}; struct input_event ev {}; - link_devnode(touch_dev.get(), touch_el.get()); + link_devnode(mouse_abs.get(), touch_el.get()); - auto rc = libevdev_next_event(touch_dev.get(), LIBEVDEV_READ_FLAG_NORMAL, &ev); + auto rc = libevdev_next_event(mouse_abs.get(), LIBEVDEV_READ_FLAG_NORMAL, &ev); REQUIRE(rc == -EAGAIN); auto mv_packet = data::MOUSE_MOVE_ABS_PACKET{.x = boost::endian::native_to_big((short)10), .y = boost::endian::native_to_big((short)20), .width = boost::endian::native_to_big((short)1920), .height = boost::endian::native_to_big((short)1080)}; - mouse::move_touchpad(touch_el.get(), mv_packet); + mouse::move_mouse_abs(touch_el.get(), mv_packet); - rc = libevdev_next_event(touch_dev.get(), LIBEVDEV_READ_FLAG_NORMAL, &ev); + rc = libevdev_next_event(mouse_abs.get(), LIBEVDEV_READ_FLAG_NORMAL, &ev); REQUIRE(rc == LIBEVDEV_READ_STATUS_SUCCESS); REQUIRE_THAT(libevdev_event_type_get_name(ev.type), Equals("EV_ABS")); REQUIRE_THAT(libevdev_event_code_get_name(ev.type, ev.code), Equals("ABS_X")); From 731fe947675f4d36decbe38ae68bb4b95757c96a Mon Sep 17 00:00:00 2001 From: ABeltramo Date: Fri, 20 Jan 2023 19:22:02 +0000 Subject: [PATCH 14/25] fix: print process error --- src/wolf/process/process.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/wolf/process/process.cpp b/src/wolf/process/process.cpp index eae7102e..ba3f2e05 100644 --- a/src/wolf/process/process.cpp +++ b/src/wolf/process/process.cpp @@ -50,8 +50,9 @@ void run_process(immer::box process_ev) { auto ex_code = child_proc.exit_code(); logs::log(logs::debug, "[PROCESS] Terminated with status code: {}\nstd_out: {}", ex_code, std_out.get()); - if (!err_out.get().empty()) { - logs::log(logs::warning, "[PROCESS] Terminated with status code: {}, std_err: {}", ex_code, err_out.get()); + auto errors = err_out.get(); + if (!errors.empty()) { + logs::log(logs::warning, "[PROCESS] Terminated with status code: {}, std_err: {}", ex_code, errors); } terminate_handler.unregister(); From 44666dada9a5f48dcae4d8b4d4a490f9107f3af6 Mon Sep 17 00:00:00 2001 From: ABeltramo Date: Fri, 20 Jan 2023 19:55:02 +0000 Subject: [PATCH 15/25] feat: docker build gstreamer, testing compose --- .dockerignore | 12 ++-- .github/workflows/docker-build.yml | 17 ++++- Dockerfile | 10 ++- docker/.dockerignore | 2 + docker/docker-compose.yml | 100 +++++++++++++++++++++++++++++ docker/gstreamer.Dockerfile | 92 ++++++++++++++++++++++++++ docker/pulse-setup.sh | 12 ++++ 7 files changed, 233 insertions(+), 12 deletions(-) create mode 100644 docker/.dockerignore create mode 100644 docker/docker-compose.yml create mode 100644 docker/gstreamer.Dockerfile create mode 100644 docker/pulse-setup.sh diff --git a/.dockerignore b/.dockerignore index 204a96d0..e3c93b6c 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,5 +1,7 @@ -cmake-build* -docs/ -.github/ -.dockerignore -Dockerfile \ No newline at end of file +# Start by removing everything and add just the things we need +* + +!src +!cmake +!CMakeLists.txt +!.clang-format \ No newline at end of file diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index 963278a1..077fab58 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -66,6 +66,7 @@ jobs: type=semver,pattern={{major}} type=edge,branch=master type=ref,event=branch + type=raw,value=alpha type=sha flavor: latest=false # let's not produce a :latest tag @@ -92,11 +93,25 @@ jobs: username: ${{ github.repository_owner }} password: ${{ secrets.GHCR_TOKEN }} - - name: Build and Push + - name: Build Gstreamer id: docker_build uses: docker/build-push-action@v3 with: builder: ${{ steps.buildx.outputs.name }} + context: docker + file: gstreamer.Dockerfile + push: true + tags: ghcr.io/${{ github.repository_owner }}/gstreamer:1.20.3,gameonwhales:1.20.3 # TODO: set gstreamer version as param + cache-from: registry,ref=ghcr.io/${{ github.repository_owner }}/gstreamer:buildcache-1.20.3 + cache-to: registry,ref=ghcr.io/${{ github.repository_owner }}/gstreamer:buildcache-1.20.3,mode=max + + - name: Build Wolf + id: docker_build + uses: docker/build-push-action@v3 + with: + builder: ${{ steps.buildx.outputs.name }} + context: . + file: Dockerfile push: ${{ steps.prep.outputs.push }} tags: ${{ steps.meta.outputs.tags }} build-args: | diff --git a/Dockerfile b/Dockerfile index 12424f81..3706f620 100644 --- a/Dockerfile +++ b/Dockerfile @@ -23,6 +23,8 @@ COPY cmake /wolf/cmake COPY CMakeLists.txt /wolf/CMakeLists.txt WORKDIR /wolf +# TODO: link with gstreamer from gameonwhales/gstreamer:1.20.3 + ENV CCACHE_DIR=/cache/ccache ENV CMAKE_BUILD_DIR=/cache/cmake-build RUN --mount=type=cache,target=/cache/ccache \ @@ -62,20 +64,18 @@ RUN --mount=type=cache,target=/usr/local/cargo/registry cargo build --release ######################################################## -# TODO: build gstreamer plugin manually -######################################################## -FROM ubuntu:22.04 AS runner +FROM gameonwhales/gstreamer:1.20.3 AS runner ENV DEBIAN_FRONTEND=noninteractive # Wolf runtime dependencies # curl only used by plugin curlhttpsrc (remote video play) RUN apt-get update -y && \ apt-get install -y --no-install-recommends \ - gstreamer1.0-plugins-base gstreamer1.0-plugins-good gstreamer1.0-plugins-ugly gstreamer1.0-plugins-bad \ ca-certificates libcurl4 \ tini \ libssl3 \ libevdev2 \ + va-driver-all \ && rm -rf /var/lib/apt/lists/* # gst-plugin-wayland runtime dependencies @@ -87,8 +87,6 @@ RUN apt-get update -y && \ # TODO: avoid running as root -# DEBUG: install gstreamer1.0-tools in order to add gst-inspect-1.0, gst-launch-1.0 and more -ENV GST_PLUGIN_PATH=/usr/lib/x86_64-linux-gnu/gstreamer-1.0/ COPY --from=wolf-builder /wolf/wolf /wolf/wolf COPY --from=gst-plugin-wayland /sunrise/gst-plugin-wayland-display/target/release/libgstwaylanddisplay.so $GST_PLUGIN_PATH/libgstwaylanddisplay.so diff --git a/docker/.dockerignore b/docker/.dockerignore new file mode 100644 index 00000000..6ada06a6 --- /dev/null +++ b/docker/.dockerignore @@ -0,0 +1,2 @@ +# Start by removing everything and add just the things we need +* \ No newline at end of file diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml new file mode 100644 index 00000000..0b88cbbb --- /dev/null +++ b/docker/docker-compose.yml @@ -0,0 +1,100 @@ +version: "3" + +services: + ############################## + # Wolf + ########## + wolf: + image: ghcr.io/games-on-whales/wolf:alpha + depends_on: + - xorg + - pulse + - udevd + ports: + - "47984:47984/tcp" + - "47989:47989/tcp" + - "48010:48010/tcp" + - "47998-48000:47998-48000/udp" + privileged: true + ipc: service:xorg + volumes: + - xorg:/tmp/.X11-unix + - pulse:/tmp/pulse/ + - /home/ale/wolf:/wolf/cfg # TODO: set this + devices: + - /dev/dri:/dev/dri + environment: + LOG_LEVEL: "INFO" # Wolf: Set to DEBUG to print more + GST_DEBUG: "2" # Gstreamer: 2 logs all warnings and errors, set to 4 to see debug information + + XDG_RUNTIME_DIR: "/tmp/.X11-unix" + XORG_SOCKET: "/tmp/.X11-unix/X0" + DISPLAY: ":0" + PULSE_SERVER: "unix:/tmp/pulse/pulse-socket" + LIBVA_DRIVER_NAME: "iHD" # Might be needed by VAAPI + + ############################## + # An app running + ########## + firefox: + image: andrewmackrodt/firefox-x11 + depends_on: + - xorg + volumes: + - xorg:/tmp/.X11-unix + - pulse:/tmp/pulse/ + - ffox_state:/run/user/1000 + environment: + LOG_LEVEL: info + XDG_RUNTIME_DIR: "/tmp/.X11-unix" + DISPLAY: ":0" + PULSE_SERVER: "unix:/tmp/pulse/pulse-socket" + + ############################## + # Desktop stack + ########## + xorg: + image: ghcr.io/games-on-whales/xorg:edge + network_mode: service:udevd + privileged: true + volumes: + - /dev/input:/dev/input:ro + - udev:/run/udev/:ro + - xorg:/tmp/.X11-unix + - /run/dbus/system_bus_socket:/run/dbus/system_bus_socket:ro + ipc: shareable # Needed for MIT-SHM, removing this should cause a performance hit see https://github.com/jessfraz/dockerfiles/issues/359 + devices: + - /dev/dri:/dev/dri + environment: + RESOLUTION: "1920x1080" + DISPLAY: ":0" + CURRENT_OUTPUT: "HDMI-1" # TODO: adjust this accordingly + REFRESH_RATE: "60" + FORCE_RESOLUTION: "false" + LIBVA_DRIVERS_PATH: /usr/lib/x86_64-linux-gnu/dri/ + LIBVA_DRIVER_NAME: "iHD" + + pulse: + command: /bin/bash -c "chmod +x /home/retro/entrypoint.sh && /home/retro/entrypoint.sh" + image: ghcr.io/games-on-whales/pulseaudio:edge + depends_on: + - xorg + ipc: service:xorg + volumes: + - pulse:/tmp/pulse/ + - /run/dbus/system_bus_socket:/run/dbus/system_bus_socket:ro + - ./pulse-setup.sh:/home/retro/entrypoint.sh + + udevd: + image: ghcr.io/games-on-whales/udevd:edge + network_mode: host + privileged: true + volumes: + - udev:/run/udev/ + + +volumes: + udev: + xorg: + pulse: + ffox_state: \ No newline at end of file diff --git a/docker/gstreamer.Dockerfile b/docker/gstreamer.Dockerfile new file mode 100644 index 00000000..de442886 --- /dev/null +++ b/docker/gstreamer.Dockerfile @@ -0,0 +1,92 @@ +FROM ubuntu:22.04 AS builder + +ENV DEBIAN_FRONTEND=noninteractive + + +RUN apt-get update -y && \ + apt-get install -y --no-install-recommends \ + ca-certificates \ + git \ + build-essential \ + meson bison ninja-build \ + flex libx265-dev nasm libva-dev libzxingcore-dev libzbar-dev \ + libx11-dev libwayland-dev libpulse-dev \ + && rm -rf /var/lib/apt/lists/* + +###################################################### +# Install nvidia cuda toolkit +# Adapted from: https://developer.nvidia.com/cuda-downloads?target_os=Linux&target_arch=x86_64&Distribution=Ubuntu&target_version=22.04&target_type=deb_local +# +# Since 1.17.1, the `nvcodec` plugin does not need access to the Nvidia Video SDK +# or the CUDA SDK. It now loads everything at runtime. Hence, it is now enabled +# by default on all platforms. + +#WORKDIR /cuda +# +#RUN wget https://developer.download.nvidia.com/compute/cuda/repos/ubuntu2204/x86_64/cuda-ubuntu2204.pin && \ +# mv cuda-ubuntu2204.pin /etc/apt/preferences.d/cuda-repository-pin-600 && \ +# wget https://developer.download.nvidia.com/compute/cuda/12.0.0/local_installers/cuda-repo-ubuntu2204-12-0-local_12.0.0-525.60.13-1_amd64.deb && \ +# dpkg -i cuda-repo-ubuntu2204-12-0-local_12.0.0-525.60.13-1_amd64.deb && \ +# cp /var/cuda-repo-ubuntu2204-12-0-local/cuda-*-keyring.gpg /usr/share/keyrings/ && \ +# apt-get update && \ +# apt-get install -y cuda && \ +# rm cuda-repo-ubuntu2204-12-0-local_12.0.0-525.60.13-1_amd64.deb && \ +# rm -rf /var/lib/apt/lists/* + +###################################################### +# Build gstreamer plugins +# see the list of possible options here: https://gitlab.freedesktop.org/gstreamer/gstreamer/-/blob/main/meson_options.txt + +WORKDIR /gstreamer +RUN git clone https://gitlab.freedesktop.org/gstreamer/gstreamer.git + +ARG GSTREAMER_VERSION=1.20.3 +ENV GSTREAMER_VERSION=$GSTREAMER_VERSION +WORKDIR /gstreamer/gstreamer +RUN git checkout $GSTREAMER_VERSION + +# vaapi can't be added to the static build +# Can't make a static build: GStreamer-VAAPI plugin not supported with `static` builds yet. + +RUN meson setup \ + --buildtype=release \ + --strip \ + -Dgst-full-libraries=app,video \ + -Dgpl=enabled \ + -Dbase=enabled \ + -Dgood=enabled \ + -Dugly=enabled \ + -Drs=disabled \ + -Dtls=disabled \ + -Dgst-examples=disabled \ + -Dlibav=disabled \ + -Dtests=disabled \ + -Dexamples=disabled \ + -Ddoc=disabled \ + -Dpython=disabled \ + -Drtsp_server=disabled \ + -Dqt5=disabled \ + -Dbad=enabled \ + -Dgst-plugins-good:ximagesrc=enabled \ + -Dgst-plugins-good:pulse=enabled \ + -Dgst-plugins-bad:x265=enabled \ + -Dgst-plugin-bad:nvcodec=enabled \ + -Dvaapi=enabled \ + build + +RUN ninja -C build + +FROM ubuntu:22.04 +ENV DEBIAN_FRONTEND=noninteractive + +RUN apt-get update -y && \ + apt-get install -y --no-install-recommends \ + libgstreamer1.0-0 \ + libcairo-gobject2 libgdk-pixbuf-2.0-0 libva2 libva-x11-2 libva-drm2 liblcms2-2 \ + libopenexr25 libzbar0 libopenjp2-7 librsvg2-2 libx265-199 libzxingcore1 libopenh264-6 libpulse0 \ + && rm -rf /var/lib/apt/lists/* + +COPY --from=builder /gstreamer/gstreamer/build/subprojects/ /gstreamer/ + +ENV GST_PLUGIN_PATH=/gstreamer/ +CMD ["/gstreamer/gstreamer/tools/gst-inspect-1.0"] diff --git a/docker/pulse-setup.sh b/docker/pulse-setup.sh new file mode 100644 index 00000000..c173fde2 --- /dev/null +++ b/docker/pulse-setup.sh @@ -0,0 +1,12 @@ +#!/bin/bash + +pulseaudio --log-level=1 & + +sleep 1 +# We have to create a sink in order to be picked up by pulsesrc +pacmd load-module module-null-sink sink_name=MySink +pacmd update-sink-proplist MySink device.description=MySink + +# Check that we get a valid source using: pacmd list-sources + +wait -n \ No newline at end of file From 10069e4c9556173f01dc4565c4495a4415c67a2a Mon Sep 17 00:00:00 2001 From: ABeltramo Date: Fri, 20 Jan 2023 21:29:34 +0000 Subject: [PATCH 16/25] fix: github actions to build containers --- .github/workflows/docker-build.yml | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index 077fab58..cc9a6e04 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -94,19 +94,17 @@ jobs: password: ${{ secrets.GHCR_TOKEN }} - name: Build Gstreamer - id: docker_build uses: docker/build-push-action@v3 with: builder: ${{ steps.buildx.outputs.name }} context: docker - file: gstreamer.Dockerfile + file: docker/gstreamer.Dockerfile push: true - tags: ghcr.io/${{ github.repository_owner }}/gstreamer:1.20.3,gameonwhales:1.20.3 # TODO: set gstreamer version as param - cache-from: registry,ref=ghcr.io/${{ github.repository_owner }}/gstreamer:buildcache-1.20.3 - cache-to: registry,ref=ghcr.io/${{ github.repository_owner }}/gstreamer:buildcache-1.20.3,mode=max + tags: ghcr.io/${{ github.repository_owner }}/gstreamer:1.20.3,gameonwhales/gstreamer:1.20.3 # TODO: set gstreamer version as param + cache-from: type=registry,ref=ghcr.io/${{ github.repository_owner }}/gstreamer:buildcache-1.20.3 + cache-to: type=registry,ref=ghcr.io/${{ github.repository_owner }}/gstreamer:buildcache-1.20.3,mode=max - name: Build Wolf - id: docker_build uses: docker/build-push-action@v3 with: builder: ${{ steps.buildx.outputs.name }} From 93a006a18fa4d8b35bc229e1158f235fa42fb0ee Mon Sep 17 00:00:00 2001 From: ABeltramo Date: Sat, 21 Jan 2023 09:44:46 +0000 Subject: [PATCH 17/25] fix: mouse grab on x11 --- docker/gstreamer.Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/gstreamer.Dockerfile b/docker/gstreamer.Dockerfile index de442886..d3f650d3 100644 --- a/docker/gstreamer.Dockerfile +++ b/docker/gstreamer.Dockerfile @@ -10,7 +10,7 @@ RUN apt-get update -y && \ build-essential \ meson bison ninja-build \ flex libx265-dev nasm libva-dev libzxingcore-dev libzbar-dev \ - libx11-dev libwayland-dev libpulse-dev \ + libx11-dev libxfixes-dev libxdamage-dev libwayland-dev libpulse-dev \ && rm -rf /var/lib/apt/lists/* ###################################################### From 01a89c551711d0c5fdb7dd2e888c816fbbb5eb0d Mon Sep 17 00:00:00 2001 From: ABeltramo Date: Sun, 22 Jan 2023 10:24:40 +0000 Subject: [PATCH 18/25] feat: async pair via web page --- src/wolf/CMakeLists.txt | 10 +++ src/wolf/rest/endpoints.hpp | 49 +++++++------- src/wolf/rest/html/pin.html | 99 +++++++++++++++++++++++++++ src/wolf/rest/html/pin.include.html | 100 ++++++++++++++++++++++++++++ src/wolf/rest/servers.cpp | 52 +++++++++++++-- src/wolf/state/data-structures.hpp | 7 +- src/wolf/wolf.cpp | 10 --- 7 files changed, 287 insertions(+), 40 deletions(-) create mode 100644 src/wolf/rest/html/pin.html create mode 100644 src/wolf/rest/html/pin.include.html diff --git a/src/wolf/CMakeLists.txt b/src/wolf/CMakeLists.txt index 188e0b9d..2aaff751 100644 --- a/src/wolf/CMakeLists.txt +++ b/src/wolf/CMakeLists.txt @@ -100,6 +100,16 @@ target_sources(wolf_runner PUBLIC ${HEADER_LIST}) +## Add pin.html +function(make_includable input_file output_file) + file(READ ${input_file} content) + set(delim "for_c++_include") + set(content "R\"${delim}(\n${content})${delim}\"") + file(WRITE ${output_file} "${content}") +endfunction(make_includable) + +make_includable(rest/html/pin.html rest/html/pin.include.html) + # All users of this library will need at least C++17 target_compile_features(wolf_runner PUBLIC cxx_std_17) diff --git a/src/wolf/rest/endpoints.hpp b/src/wolf/rest/endpoints.hpp index 8dec6afa..1d1eff8a 100644 --- a/src/wolf/rest/endpoints.hpp +++ b/src/wolf/rest/endpoints.hpp @@ -76,30 +76,33 @@ void pair(std::shared_ptr::Response> response, // PHASE 1 if (client_id && salt && client_cert_str) { - auto future_pin = std::promise(); - state::PairSignal signal = {client_ip, future_pin}; - state->event_bus->fire_event(signal); // Emit a signal and wait for a response - auto user_pin = future_pin.get_future().get(); + auto future_pin = std::make_shared>(); + state->event_bus->fire_event( // Emit a signal and wait for the promise to be fulfilled + immer::box(state::PairSignal{.client_ip = client_ip, .user_pin = future_pin})); + + future_pin->get_future().then( + [state, salt, client_cert_str, cache_key, client_id, response](boost::future fut_pin) { + auto server_pem = x509::get_cert_pem(*state->host.server_cert); + auto result = moonlight::pair::get_server_cert(fut_pin.get(), salt.value(), server_pem); + + auto client_cert_parsed = crypto::hex_to_str(client_cert_str.value(), true); + + state->pairing_cache.update([&](const immer::map &pairing_cache) { + return pairing_cache.set(cache_key, + {client_id.value(), + client_cert_parsed, + state::RTSP_SETUP_PORT, + state::CONTROL_PORT, + state::VIDEO_STREAM_PORT, + state::AUDIO_STREAM_PORT, + result.second, + std::nullopt, + std::nullopt}); + }); + + send_xml(response, SimpleWeb::StatusCode::success_ok, result.first); + }); - auto server_pem = x509::get_cert_pem(*state->host.server_cert); - auto result = moonlight::pair::get_server_cert(user_pin, salt.value(), server_pem); - - auto client_cert_parsed = crypto::hex_to_str(client_cert_str.value(), true); - - state->pairing_cache.update([&](const immer::map &pairing_cache) { - return pairing_cache.set(cache_key, - {client_id.value(), - client_cert_parsed, - state::RTSP_SETUP_PORT, - state::CONTROL_PORT, - state::VIDEO_STREAM_PORT, - state::AUDIO_STREAM_PORT, - result.second, - std::nullopt, - std::nullopt}); - }); - - send_xml(response, SimpleWeb::StatusCode::success_ok, result.first); return; } diff --git a/src/wolf/rest/html/pin.html b/src/wolf/rest/html/pin.html new file mode 100644 index 00000000..d648c378 --- /dev/null +++ b/src/wolf/rest/html/pin.html @@ -0,0 +1,99 @@ + + + + + Wolf - PIN + + + + + +
+
+ + + + +
+
+ + + + + \ No newline at end of file diff --git a/src/wolf/rest/html/pin.include.html b/src/wolf/rest/html/pin.include.html new file mode 100644 index 00000000..f916c95a --- /dev/null +++ b/src/wolf/rest/html/pin.include.html @@ -0,0 +1,100 @@ +R"for_c++_include( + + + + + Wolf - PIN + + + + + +
+
+ + + + +
+
+ + + + +)for_c++_include" \ No newline at end of file diff --git a/src/wolf/rest/servers.cpp b/src/wolf/rest/servers.cpp index d5926001..ec7423f4 100644 --- a/src/wolf/rest/servers.cpp +++ b/src/wolf/rest/servers.cpp @@ -1,4 +1,5 @@ -#include +#include +#include #include #include #include @@ -6,6 +7,15 @@ namespace HTTPServers { +/** + * A bit of magic here, it'll load up the pin.html via Cmake (look for `make_includable`) + */ +constexpr char const *pin_html = +#include "html/pin.include.html" + ; + +namespace bt = boost::property_tree; + /** * @brief Start the generic server on the specified port * @return std::thread: the thread where this server will run @@ -24,15 +34,36 @@ std::thread startServer(HttpServer *server, const std::shared_ptr(resp, req, state); }; + auto pairing_atom = std::make_shared>>>(); + + server->resource["^/pin/$"]["GET"] = [](auto resp, auto req) { resp->write(pin_html); }; + server->resource["^/pin/$"]["POST"] = [pairing_atom](auto resp, auto req) { + try { + bt::ptree pt; + + read_json(req->content, pt); + + auto pin = pt.get("pin"); + auto secret = pt.get("secret"); + logs::log(logs::debug, "Received POST /pin/ pin:{} secret:{}", pin, secret); + + auto pair_request = pairing_atom->load()->at(secret); + pair_request->user_pin->set_value(pin); + resp->write("OK"); + pairing_atom->update([&secret](auto m) { return m.erase(secret); }); + } catch (const std::exception &e) { + *resp << "HTTP/1.1 400 Bad Request\r\nContent-Length: " << strlen(e.what()) << "\r\n\r\n" << e.what(); + } + }; + server->resource["^/unpair$"]["GET"] = [&state](auto resp, auto req) { SimpleWeb::CaseInsensitiveMultimap headers = req->parse_query_string(); auto client_id = get_header(headers, "uniqueid"); auto client_ip = req->remote_endpoint().address().to_string(); auto cache_key = client_id.value() + "@" + client_ip; - state->pairing_cache.update([&cache_key](const immer::map &pairing_cache) { - return pairing_cache.erase(cache_key); - }); + logs::log(logs::info, "Unpairing: {}", cache_key); + state::unpair(state->config, state->pairing_cache.load()->at(cache_key)); XML xml; xml.put("root..status_code", 200); @@ -40,9 +71,20 @@ std::thread startServer(HttpServer *server, const std::shared_ptrevent_bus](auto server) { + auto pair_handler = event_bus->register_handler>( + [pairing_atom](const immer::box &pair_sig) { + pairing_atom->update([&pair_sig](auto m) { + auto secret = crypto::str_to_hex(crypto::random(8)); + logs::log(logs::info, "Insert pin at http://localhost:47989/pin/#{}", secret); + return m.set(secret, pair_sig); + }); + }); + // Start server server->start([](unsigned short port) { logs::log(logs::info, "HTTP server listening on port: {} ", port); }); + + pair_handler.unregister(); }, server); diff --git a/src/wolf/state/data-structures.hpp b/src/wolf/state/data-structures.hpp index f8b887f3..3aef1e19 100644 --- a/src/wolf/state/data-structures.hpp +++ b/src/wolf/state/data-structures.hpp @@ -2,7 +2,6 @@ #include #include -#include #include #include #include @@ -13,6 +12,10 @@ #include #include +#define BOOST_THREAD_PROVIDES_FUTURE_CONTINUATION +#define BOOST_THREAD_PROVIDES_FUTURE +#include + namespace state { using namespace std::chrono_literals; @@ -40,7 +43,7 @@ struct PairedClient { struct PairSignal { std::string client_ip; - std::promise &user_pin; + std::shared_ptr> user_pin; }; using PairedClientList = immer::vector>; diff --git a/src/wolf/wolf.cpp b/src/wolf/wolf.cpp index f7cf7507..6ea3a611 100644 --- a/src/wolf/wolf.cpp +++ b/src/wolf/wolf.cpp @@ -110,13 +110,6 @@ const char *get_env(const char *tag, const char *def = nullptr) noexcept { return ret ? ret : def; } -void user_pin_handler(state::PairSignal pair_request) { - std::string user_pin; - std::cout << "Insert pin:" << std::endl; - std::getline(std::cin, user_pin); - pair_request.user_pin.set_value(user_pin); -} - /** * A bit tricky here: on a basic level we want a map of [session_id] -> thread_pool */ @@ -133,9 +126,6 @@ typedef immer::atom &event_bus, TreadsMapAtom &threads) { immer::vector_transient> handlers; - // HTTP PIN - handlers.push_back(event_bus->register_handler(&user_pin_handler)); - // RTSP handlers.push_back(event_bus->register_handler>( [&threads](immer::box stream_session) { From f0d007d127b4922f3fc17f14d39c3770f82cbe76 Mon Sep 17 00:00:00 2001 From: ABeltramo Date: Sun, 22 Jan 2023 13:41:06 +0000 Subject: [PATCH 19/25] feat: better default config, added VAAPI and X11 --- docker/docker-compose.yml | 5 +- src/wolf/state/configTOML.cpp | 263 +++++++++++++++++----------------- 2 files changed, 133 insertions(+), 135 deletions(-) diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 0b88cbbb..869e031c 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -20,7 +20,7 @@ services: volumes: - xorg:/tmp/.X11-unix - pulse:/tmp/pulse/ - - /home/ale/wolf:/wolf/cfg # TODO: set this + - wolf_state:/wolf/cfg # Mount this externally to see and edit config file devices: - /dev/dri:/dev/dri environment: @@ -97,4 +97,5 @@ volumes: udev: xorg: pulse: - ffox_state: \ No newline at end of file + ffox_state: + wolf_state: \ No newline at end of file diff --git a/src/wolf/state/configTOML.cpp b/src/wolf/state/configTOML.cpp index b551102d..63799bf5 100644 --- a/src/wolf/state/configTOML.cpp +++ b/src/wolf/state/configTOML.cpp @@ -49,141 +49,138 @@ void write(const toml::value &data, const std::string &dest) { out_file.close(); } -Config get_default() { - state::PairedClientList clients = {}; - auto atom = new immer::atom(clients); - return Config{ - .uuid = gen_uuid(), - .hostname = "wolf", - .support_hevc = false, - .paired_clients = *atom, - .apps = {{.base = {"Test ball", "1", true}, - .h264_gst_pipeline = video::DEFAULT_SOURCE.data() + " ! "s + video::DEFAULT_PARAMS.data() + " ! "s + - video::DEFAULT_H264_ENCODER.data() + " ! " + video::DEFAULT_SINK.data(), - .hevc_gst_pipeline = video::DEFAULT_SOURCE.data() + " ! "s + video::DEFAULT_PARAMS.data() + " ! "s + - video::DEFAULT_H265_ENCODER.data() + " ! " + video::DEFAULT_SINK.data(), - .opus_gst_pipeline = audio::DEFAULT_SOURCE.data() + " ! "s + audio::DEFAULT_PARAMS.data() + " ! "s + - audio::DEFAULT_OPUS_ENCODER.data() + " ! " + audio::DEFAULT_SINK.data(), - .run_cmd = "sh -c \"while :; do echo 'running...'; sleep 1; done\""}}}; -} - Config load_or_default(const std::string &source) { - if (file_exist(source)) { - const auto cfg = toml::parse(source); - - auto uuid = toml::find_or(cfg, "uuid", gen_uuid()); - auto hostname = toml::find_or(cfg, "hostname", "Wolf"); - - GstVideoCfg default_gst_video_settings = toml::find(cfg, "gstreamer", "video"); - GstAudioCfg default_gst_audio_settings = toml::find(cfg, "gstreamer", "audio"); - - auto cfg_clients = toml::find>(cfg, "paired_clients"); - auto paired_clients = - cfg_clients // - | ranges::views::transform([](const PairedClient &client) { return immer::box{client}; }) // - | ranges::to>>(); - - auto cfg_apps = toml::find>(cfg, "apps"); - auto apps = - cfg_apps // - | ranges::views::enumerate // - | ranges::views::transform([&default_gst_audio_settings, - &default_gst_video_settings](std::pair pair) { - auto [idx, item] = pair; - auto h264_gst_pipeline = - toml::find_or(item, "video", "source", default_gst_video_settings.default_source) + " ! " + - toml::find_or(item, - "video", - "video_params", - default_gst_video_settings.default_video_params) + - " ! " + - toml::find_or(item, - "video", - "h264_encoder", - default_gst_video_settings.default_h264_encoder) + - " ! " + toml::find_or(item, "video", "sink", default_gst_video_settings.default_sink); - - auto hevc_gst_pipeline = - toml::find_or(item, "video", "source", default_gst_video_settings.default_source) + " ! " + - toml::find_or(item, - "video", - "video_params", - default_gst_video_settings.default_video_params) + - " ! " + - toml::find_or(item, - "video", - "hevc_encoder", - default_gst_video_settings.default_hevc_encoder) + - " ! " + toml::find_or(item, "video", "sink", default_gst_video_settings.default_sink); - - auto opus_gst_pipeline = - toml::find_or(item, "audio", "source", default_gst_audio_settings.default_source) + " ! " + - toml::find_or(item, - "audio", - "video_params", - default_gst_audio_settings.default_audio_params) // - + " ! " + // - toml::find_or(item, - "audio", - "opus_encoder", - default_gst_audio_settings.default_opus_encoder) // - + " ! " + // - toml::find_or(item, "audio", "sink", default_gst_audio_settings.default_sink); - - auto run_cmd = - toml::find_or(item, "run_cmd", "sh -c \"while :; do echo 'running...'; sleep 1; done\""); - - return state::App{.base = {.title = toml::find(item, "title"), - .id = std::to_string(idx + 1), // Moonlight expects: 1,2,3 ... - .support_hdr = toml::find_or(item, "support_hdr", false)}, - .h264_gst_pipeline = h264_gst_pipeline, - .hevc_gst_pipeline = hevc_gst_pipeline, - .opus_gst_pipeline = opus_gst_pipeline, - .run_cmd = run_cmd}; - }) // - | ranges::to>(); // - - auto clients_atom = new immer::atom(paired_clients); - return Config{.uuid = uuid, - .hostname = hostname, - .config_source = source, - .support_hevc = toml::find_or(cfg, "support_hevc", false), - .paired_clients = *clients_atom, - .apps = apps}; - - } else { - logs::log(logs::warning, "Unable to open config file: {}, creating one using defaults", source); - - auto cfg = get_default(); - cfg.config_source = source; - - const toml::value data = {{"uuid", cfg.uuid}, - {"hostname", cfg.hostname}, - {"support_hevc", cfg.support_hevc}, - {"paired_clients", toml::array{}}, - {"apps", cfg.apps}, - {"gstreamer", // key - { // array - { - "video", - {{"default_source", video::DEFAULT_SOURCE}, - {"default_video_params", video::DEFAULT_PARAMS}, - {"default_h264_encoder", video::DEFAULT_H264_ENCODER}, - {"default_hevc_encoder", video::DEFAULT_H265_ENCODER}, - {"default_sink", video::DEFAULT_SINK}}, - }, - { - "audio", - {{"default_source", audio::DEFAULT_SOURCE}, - {"default_audio_params", audio::DEFAULT_PARAMS}, - {"default_opus_encoder", audio::DEFAULT_OPUS_ENCODER}, - {"default_sink", audio::DEFAULT_SINK}}, - }}}}; - - write(data, source); // write it back for future users - - return cfg; + if (!file_exist(source)) { + { + logs::log(logs::warning, "Unable to open config file: {}, creating one using defaults", source); + + auto video_test = "videotestsrc pattern=ball is-live=true"; + auto x11_src = "ximagesrc show-pointer=true use-damage=false"; + auto pulse_src = "pulsesrc"; + + auto default_app = toml::value{{"title", "Test ball"}, {"video", {{"source", video_test}}}}; + auto x11_sw = + toml::value{{"title", "X11 (SW)"}, {"video", {{"source", x11_src}}}, {"audio", {{"source", pulse_src}}}}; + + auto h264_vaapi = "vaapih264enc max-bframes=0 refs=1 num-slices={slices_per_frame} bitrate={bitrate} ! " + "video/x-h264, profile=high, stream-format=byte-stream"; + auto hevc_vaapi = "vaapih265enc max-bframes=0 refs=1 num-slices={slices_per_frame} bitrate={bitrate} ! " + "video/x-h265, profile=main, stream-format=byte-stream"; + auto video_vaapi = "videoscale ! videoconvert ! videorate ! " + "video/x-raw, framerate={fps}/1, chroma-site={color_range}, width={width}, height={height}, " + "format=NV12, colorimetry={color_space} ! " + "vaapipostproc"; + + auto test_vaapi = toml::value{{"title", "Test ball (VAAPI)"}, + {"video", + {{"source", video_test}, + {"h264_encoder", h264_vaapi}, + {"hevc_encoder", hevc_vaapi}, + {"video_params", video_vaapi}}}}; + auto x11_vaapi = toml::value{{"title", "X11 (VAAPI)"}, + {"video", + {{"source", x11_src}, + {"h264_encoder", h264_vaapi}, + {"hevc_encoder", hevc_vaapi}, + {"video_params", video_vaapi}}}, + {"audio", {{"source", pulse_src}}}}; + + const toml::value data = {{"uuid", gen_uuid()}, + {"hostname", "Wolf"}, + {"support_hevc", true}, + {"paired_clients", toml::array{}}, + {"apps", {default_app, x11_sw, test_vaapi, x11_vaapi}}, + {"gstreamer", // key + { // array + { + "video", + {{"default_source", video::DEFAULT_SOURCE}, + {"default_video_params", video::DEFAULT_PARAMS}, + {"default_h264_encoder", video::DEFAULT_H264_ENCODER}, + {"default_hevc_encoder", video::DEFAULT_H265_ENCODER}, + {"default_sink", video::DEFAULT_SINK}}, + }, + { + "audio", + {{"default_source", audio::DEFAULT_SOURCE}, + {"default_audio_params", audio::DEFAULT_PARAMS}, + {"default_opus_encoder", audio::DEFAULT_OPUS_ENCODER}, + {"default_sink", audio::DEFAULT_SINK}}, + }}}}; + + write(data, source); // write it back + } } + const auto cfg = toml::parse(source); + + auto uuid = toml::find_or(cfg, "uuid", gen_uuid()); + auto hostname = toml::find_or(cfg, "hostname", "Wolf"); + + GstVideoCfg default_gst_video_settings = toml::find(cfg, "gstreamer", "video"); + GstAudioCfg default_gst_audio_settings = toml::find(cfg, "gstreamer", "audio"); + + auto cfg_clients = toml::find>(cfg, "paired_clients"); + auto paired_clients = + cfg_clients // + | ranges::views::transform([](const PairedClient &client) { return immer::box{client}; }) // + | ranges::to>>(); + + auto cfg_apps = toml::find>(cfg, "apps"); + auto apps = + cfg_apps // + | ranges::views::enumerate // + | + ranges::views::transform([&default_gst_audio_settings, + &default_gst_video_settings](std::pair pair) { + auto [idx, item] = pair; + auto h264_gst_pipeline = + toml::find_or(item, "video", "source", default_gst_video_settings.default_source) + " ! " + + toml::find_or(item, "video", "video_params", default_gst_video_settings.default_video_params) + + " ! " + + toml::find_or(item, "video", "h264_encoder", default_gst_video_settings.default_h264_encoder) + + " ! " + toml::find_or(item, "video", "sink", default_gst_video_settings.default_sink); + + auto hevc_gst_pipeline = + toml::find_or(item, "video", "source", default_gst_video_settings.default_source) + " ! " + + toml::find_or(item, "video", "video_params", default_gst_video_settings.default_video_params) + + " ! " + + toml::find_or(item, "video", "hevc_encoder", default_gst_video_settings.default_hevc_encoder) + + " ! " + toml::find_or(item, "video", "sink", default_gst_video_settings.default_sink); + + auto opus_gst_pipeline = + toml::find_or(item, "audio", "source", default_gst_audio_settings.default_source) + " ! " + + toml::find_or(item, + "audio", + "video_params", + default_gst_audio_settings.default_audio_params) // + + " ! " + // + toml::find_or(item, + "audio", + "opus_encoder", + default_gst_audio_settings.default_opus_encoder) // + + " ! " + // + toml::find_or(item, "audio", "sink", default_gst_audio_settings.default_sink); + + auto run_cmd = + toml::find_or(item, "run_cmd", "sh -c \"while :; do echo 'running...'; sleep 1; done\""); + + return state::App{.base = {.title = toml::find(item, "title"), + .id = std::to_string(idx + 1), // Moonlight expects: 1,2,3 ... + .support_hdr = toml::find_or(item, "support_hdr", false)}, + .h264_gst_pipeline = h264_gst_pipeline, + .hevc_gst_pipeline = hevc_gst_pipeline, + .opus_gst_pipeline = opus_gst_pipeline, + .run_cmd = run_cmd}; + }) // + | ranges::to>(); // + + auto clients_atom = new immer::atom(paired_clients); + return Config{.uuid = uuid, + .hostname = hostname, + .config_source = source, + .support_hevc = toml::find_or(cfg, "support_hevc", false), + .paired_clients = *clients_atom, + .apps = apps}; } void pair(const Config &cfg, const PairedClient &client) { From dc0eb7f473ba97c69777a82027b220669c71bb4c Mon Sep 17 00:00:00 2001 From: ABeltramo Date: Sun, 22 Jan 2023 18:53:24 +0000 Subject: [PATCH 20/25] fix: added missing libxdamage1 --- docker/gstreamer.Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/gstreamer.Dockerfile b/docker/gstreamer.Dockerfile index d3f650d3..95606053 100644 --- a/docker/gstreamer.Dockerfile +++ b/docker/gstreamer.Dockerfile @@ -82,7 +82,7 @@ ENV DEBIAN_FRONTEND=noninteractive RUN apt-get update -y && \ apt-get install -y --no-install-recommends \ libgstreamer1.0-0 \ - libcairo-gobject2 libgdk-pixbuf-2.0-0 libva2 libva-x11-2 libva-drm2 liblcms2-2 \ + libcairo-gobject2 libgdk-pixbuf-2.0-0 libva2 libva-x11-2 libva-drm2 libxdamage1 liblcms2-2 \ libopenexr25 libzbar0 libopenjp2-7 librsvg2-2 libx265-199 libzxingcore1 libopenh264-6 libpulse0 \ && rm -rf /var/lib/apt/lists/* From d38977664d32c1dd79354c45d9b6229a9a3c9472 Mon Sep 17 00:00:00 2001 From: ABeltramo Date: Wed, 25 Jan 2023 20:29:18 +0000 Subject: [PATCH 21/25] feat: refactored gstreamer run code, added dot debug file --- src/streaming/CMakeLists.txt | 1 + src/streaming/streaming/streaming.cpp | 296 ++++++++++++++------------ 2 files changed, 166 insertions(+), 131 deletions(-) diff --git a/src/streaming/CMakeLists.txt b/src/streaming/CMakeLists.txt index 3e829b8a..17043de6 100644 --- a/src/streaming/CMakeLists.txt +++ b/src/streaming/CMakeLists.txt @@ -51,6 +51,7 @@ endif () # https://www.collabora.com/news-and-blog/news-and-events/generate-mininal-gstreamer-build-tailored-to-your-needs.html target_link_libraries(streaming PUBLIC wolf::moonlight + wolf::helpers fmt::fmt-header-only dp::eventbus ${GOBJECT_LIBRARIES} diff --git a/src/streaming/streaming/streaming.cpp b/src/streaming/streaming/streaming.cpp index 2c746337..a5b440f6 100644 --- a/src/streaming/streaming/streaming.cpp +++ b/src/streaming/streaming/streaming.cpp @@ -2,24 +2,30 @@ extern "C" { #include } +#include +#include +#include #include +#include #include #include #include #include #include -#include namespace streaming { +using gst_element_ptr = std::shared_ptr; +using gst_main_loop_ptr = std::shared_ptr; + /** * GStreamer needs to be initialised once per run * Call this method in your main. - * - * It is also possible to call the init function with two NULL arguments, - * in which case no command line options will be parsed by GStreamer. */ void init() { + /* It is also possible to call the init function with two NULL arguments, + * in which case no command line options will be parsed by GStreamer. + */ gst_init(nullptr, nullptr); GstPlugin *video_plugin = gst_plugin_load_by_name("rtpmoonlightpay_video"); @@ -31,13 +37,98 @@ void init() { reed_solomon_init(); } +static gboolean msg_handler(GstBus *bus, GstMessage *message, gpointer data) { + auto loop = (GMainLoop *)data; + switch (GST_MESSAGE_TYPE(message)) { + case GST_MESSAGE_ERROR: { + GError *err; + gchar *debug; + gst_message_parse_error(message, &err, &debug); + logs::log(logs::error, "[GSTREAMER] Pipeline error: {}", err->message); + g_error_free(err); + g_free(debug); + + /* Terminate pipeline on error */ + g_main_loop_quit(loop); + break; + } + case GST_MESSAGE_EOS: + /* end-of-stream */ + g_main_loop_quit(loop); + break; + default: + /* unhandled message */ + break; + } + + /* + * we want to be notified again the next time there is a message + * on the bus, so returning TRUE (FALSE means we want to stop watching + * for messages on the bus and our callback should not be called again) + */ + return TRUE; +} + +bool run_pipeline( + const std::string &pipeline_desc, + const std::function>(gst_element_ptr, gst_main_loop_ptr)> + &on_pipeline_ready) { + GError *error = nullptr; + gst_element_ptr pipeline(gst_parse_launch(pipeline_desc.c_str(), &error), ::gst_object_unref); + + if (!pipeline) { + logs::log(logs::error, "[GSTREAMER] Parse error: {}", error->message); + g_error_free(error); + return false; + } + + /* + * create a mainloop that runs/iterates the default GLib main context + * (context NULL), in other words: makes the context check if anything + * it watches for has happened. When a message has been posted on the + * bus, the default main context will automatically call our + * my_bus_callback() function to notify us of that message. + */ + gst_main_loop_ptr loop(g_main_loop_new(nullptr, FALSE), ::g_main_loop_unref); + + /* + * adds a watch for new message on our pipeline's message bus to + * the default GLib main context, which is the main context that our + * GLib main loop is attached to below + */ + auto bus = gst_pipeline_get_bus(GST_PIPELINE(pipeline.get())); + auto bus_watch_id = gst_bus_add_watch(bus, msg_handler, loop.get()); + gst_object_unref(bus); + + /* Let the calling thread set extra things */ + auto handlers = on_pipeline_ready(pipeline, loop); + + /* Set the pipeline to "playing" state*/ + gst_element_set_state(pipeline.get(), GST_STATE_PLAYING); + GST_DEBUG_BIN_TO_DOT_FILE_WITH_TS(reinterpret_cast(pipeline.get()), + GST_DEBUG_GRAPH_SHOW_ALL, + "pipeline-start"); + + /* The main loop will be run until someone calls g_main_loop_quit() */ + g_main_loop_run(loop.get()); + + /* Out of the main loop, clean up nicely */ + gst_element_set_state(pipeline.get(), GST_STATE_PAUSED); + gst_element_set_state(pipeline.get(), GST_STATE_READY); + gst_element_set_state(pipeline.get(), GST_STATE_NULL); + g_source_remove(bus_watch_id); + + for (const auto &handler : handlers) { + handler->unregister(); + } + + return true; +} + /** * Start VIDEO pipeline */ void start_streaming_video(immer::box video_session, unsigned short client_port) { - GstElement *pipeline; - GError *error = nullptr; - std::string color_range = (static_cast(video_session->color_range) == static_cast(state::JPEG)) ? "jpeg" : "mpeg2"; std::string color_space; @@ -53,140 +144,83 @@ void start_streaming_video(immer::box video_session, unsign break; } - // see an example pipeline at: https://gist.github.com/esrever10/7d39fe2d4163c5b2d7006495c3c911bb - pipeline = gst_parse_launch(fmt::format(video_session->gst_pipeline, - fmt::arg("width", video_session->display_mode.width), - fmt::arg("height", video_session->display_mode.height), - fmt::arg("fps", video_session->display_mode.refreshRate), - fmt::arg("bitrate", video_session->bitrate_kbps), - fmt::arg("client_port", client_port), - fmt::arg("client_ip", video_session->client_ip), - fmt::arg("payload_size", video_session->packet_size), - fmt::arg("fec_percentage", video_session->fec_percentage), - fmt::arg("min_required_fec_packets", video_session->min_required_fec_packets), - fmt::arg("slices_per_frame", video_session->slices_per_frame), - fmt::arg("color_space", color_space), - fmt::arg("color_range", color_range)) - .c_str(), - &error); - - if (!pipeline) { - g_print("Parse error: %s\n", error->message); - return; - } - - auto moonlight_plugin = gst_bin_get_by_name(reinterpret_cast(pipeline), "moonlight_pay"); - bool stop = false; - - auto ev_handler = video_session->event_bus->register_handler>( - [sess_id = video_session->session_id, &moonlight_plugin](immer::box ctrl_ev) { - if (ctrl_ev->session_id == sess_id) { - if (ctrl_ev->type == IDR_FRAME) { - // Force IDR event, see: https://github.com/centricular/gstwebrtc-demos/issues/186 - auto gst_ev = gst_event_new_custom( - GST_EVENT_CUSTOM_UPSTREAM, - gst_structure_new("GstForceKeyUnit", "all-headers", G_TYPE_BOOLEAN, TRUE, NULL)); - - gst_element_send_event(moonlight_plugin, gst_ev); + auto pipeline = fmt::format(video_session->gst_pipeline, + fmt::arg("width", video_session->display_mode.width), + fmt::arg("height", video_session->display_mode.height), + fmt::arg("fps", video_session->display_mode.refreshRate), + fmt::arg("bitrate", video_session->bitrate_kbps), + fmt::arg("client_port", client_port), + fmt::arg("client_ip", video_session->client_ip), + fmt::arg("payload_size", video_session->packet_size), + fmt::arg("fec_percentage", video_session->fec_percentage), + fmt::arg("min_required_fec_packets", video_session->min_required_fec_packets), + fmt::arg("slices_per_frame", video_session->slices_per_frame), + fmt::arg("color_space", color_space), + fmt::arg("color_range", color_range)); + + run_pipeline(pipeline, [video_session](auto pipeline, auto loop) { + /* + * The force IDR event will be triggered by the control stream. + * We have to pass this back into the gstreamer pipeline + * in order to force the encoder to produce a new IDR packet + */ + auto idr_handler = video_session->event_bus->register_handler>( + [sess_id = video_session->session_id, pipeline](immer::box ctrl_ev) { + if (ctrl_ev->session_id == sess_id) { + if (ctrl_ev->type == IDR_FRAME) { + logs::log(logs::debug, "[GSTREAMER] Forcing IDR"); + + gst_element_ptr moonlight_plugin( + gst_bin_get_by_name(reinterpret_cast(pipeline.get()), "moonlight_pay"), + ::gst_object_unref); + // Force IDR event, see: https://github.com/centricular/gstwebrtc-demos/issues/186 + auto gst_ev = gst_event_new_custom( + GST_EVENT_CUSTOM_UPSTREAM, + gst_structure_new("GstForceKeyUnit", "all-headers", G_TYPE_BOOLEAN, TRUE, NULL)); + + gst_element_send_event(moonlight_plugin.get(), gst_ev); + } } - } - }); - - auto terminate_handler = video_session->event_bus->register_handler>( - [sess_id = video_session->session_id, &stop](immer::box term_ev) { - if (term_ev->session_id == sess_id) { - g_print("Terminating Gstreamer Video pipeline\n"); - stop = true; - } - }); - - /* Set the pipeline to "playing" state*/ - gst_element_set_state(pipeline, GST_STATE_PLAYING); - - // GST_DEBUG_BIN_TO_DOT_FILE(reinterpret_cast(pipeline), GST_DEBUG_GRAPH_SHOW_STATES, "pipeline-start"); - - while (!stop) { - auto message = gst_bus_poll(GST_ELEMENT_BUS(pipeline), GST_MESSAGE_ERROR, 5 * GST_MSECOND); + }); - if (message) { - g_print("Error while running GStreamer pipeline!! \n"); - stop = true; - gst_message_unref(message); - } - } - - // GST_DEBUG_BIN_TO_DOT_FILE(reinterpret_cast(pipeline), GST_DEBUG_GRAPH_SHOW_STATES, "pipeline-end"); + auto terminate_handler = video_session->event_bus->register_handler>( + [sess_id = video_session->session_id, loop](immer::box term_ev) { + if (term_ev->session_id == sess_id) { + logs::log(logs::debug, "[GSTREAMER] Terminating video pipeline"); + g_main_loop_quit(loop.get()); + } + }); - /* Out of the main loop, clean up nicely */ - gst_element_set_state(pipeline, GST_STATE_PAUSED); - gst_element_set_state(pipeline, GST_STATE_READY); - gst_element_set_state(pipeline, GST_STATE_NULL); - - ev_handler.unregister(); - terminate_handler.unregister(); - gst_object_unref(moonlight_plugin); - gst_object_unref(GST_OBJECT(pipeline)); + return immer::array>{std::move(idr_handler), std::move(terminate_handler)}; + }); } /** * Start AUDIO pipeline */ void start_streaming_audio(immer::box audio_session, unsigned short client_port) { - GstElement *pipeline; - GError *error = nullptr; - - pipeline = gst_parse_launch(fmt::format(audio_session->gst_pipeline, - fmt::arg("channels", audio_session->channels), - fmt::arg("bitrate", audio_session->bitrate), - fmt::arg("packet_duration", audio_session->packet_duration), - fmt::arg("aes_key", audio_session->aes_key), - fmt::arg("aes_iv", audio_session->aes_iv), - fmt::arg("encrypt", audio_session->encrypt_audio), - fmt::arg("client_port", client_port), - fmt::arg("client_ip", audio_session->client_ip), - fmt::arg("stream_type", "audio")) - .c_str(), - &error); - - if (!pipeline) { - g_print("Parse error: %s\n", error->message); - return; - } - - bool stop = false; - - auto terminate_handler = audio_session->event_bus->register_handler>( - [sess_id = audio_session->session_id, &stop](immer::box term_ev) { - if (term_ev->session_id == sess_id) { - g_print("Terminating Gstreamer Video pipeline\n"); - stop = true; - } - }); - - /* Set the pipeline to "playing" state*/ - gst_element_set_state(pipeline, GST_STATE_PLAYING); - - // GST_DEBUG_BIN_TO_DOT_FILE(reinterpret_cast(pipeline), GST_DEBUG_GRAPH_SHOW_STATES, "pipeline-start"); - - while (!stop) { - auto message = gst_bus_poll(GST_ELEMENT_BUS(pipeline), GST_MESSAGE_ERROR, 5 * GST_MSECOND); - - if (message) { - g_print("Error while running GStreamer pipeline!! \n"); - stop = true; - gst_message_unref(message); - } - } - - // GST_DEBUG_BIN_TO_DOT_FILE(reinterpret_cast(pipeline), GST_DEBUG_GRAPH_SHOW_STATES, "pipeline-end"); - - /* Out of the main loop, clean up nicely */ - gst_element_set_state(pipeline, GST_STATE_PAUSED); - gst_element_set_state(pipeline, GST_STATE_READY); - gst_element_set_state(pipeline, GST_STATE_NULL); + auto pipeline = fmt::format(audio_session->gst_pipeline, + fmt::arg("channels", audio_session->channels), + fmt::arg("bitrate", audio_session->bitrate), + fmt::arg("packet_duration", audio_session->packet_duration), + fmt::arg("aes_key", audio_session->aes_key), + fmt::arg("aes_iv", audio_session->aes_iv), + fmt::arg("encrypt", audio_session->encrypt_audio), + fmt::arg("client_port", client_port), + fmt::arg("client_ip", audio_session->client_ip), + fmt::arg("stream_type", "audio")); + + run_pipeline(pipeline, [audio_session](auto pipeline, auto loop) { + auto terminate_handler = audio_session->event_bus->register_handler>( + [sess_id = audio_session->session_id, loop](immer::box term_ev) { + if (term_ev->session_id == sess_id) { + logs::log(logs::debug, "[GSTREAMER] Terminating audio pipeline"); + g_main_loop_quit(loop.get()); + } + }); - terminate_handler.unregister(); - gst_object_unref(GST_OBJECT(pipeline)); + return immer::array>{std::move(terminate_handler)}; + }); } } // namespace streaming \ No newline at end of file From d15ebe3df590bbda542a49a6f268502cf9ada088 Mon Sep 17 00:00:00 2001 From: ABeltramo Date: Fri, 27 Jan 2023 23:18:52 +0000 Subject: [PATCH 22/25] fix: crypto, replaced deprecated methods, proper cleanup where needed --- src/crypto/src/crypto.cpp | 18 +++++---- src/crypto/src/x509.cpp | 27 +++++++++---- tests/testCrypto.cpp | 81 +++++++++++++++++++++++---------------- 3 files changed, 77 insertions(+), 49 deletions(-) diff --git a/src/crypto/src/crypto.cpp b/src/crypto/src/crypto.cpp index 5fee8838..69653ca2 100644 --- a/src/crypto/src/crypto.cpp +++ b/src/crypto/src/crypto.cpp @@ -10,14 +10,18 @@ namespace crypto { std::string sha256(std::string_view str) { - unsigned char hash[SHA256_DIGEST_LENGTH]; - SHA256_CTX sha256; - SHA256_Init(&sha256); - SHA256_Update(&sha256, str.data(), str.size()); - SHA256_Final(hash, &sha256); + unsigned char md_value[EVP_MAX_MD_SIZE]; + unsigned int md_len; + auto mdctx = EVP_MD_CTX_new(); + + EVP_DigestInit_ex(mdctx, EVP_sha256(), nullptr); + EVP_DigestUpdate(mdctx, str.data(), str.size()); + EVP_DigestFinal_ex(mdctx, md_value, &md_len); + EVP_MD_CTX_free(mdctx); + std::stringstream ss; - for (unsigned char i : hash) { - ss << std::hex << std::setw(2) << std::setfill('0') << (int)i; + for (int i = 0; i < md_len; ++i) { + ss << std::hex << std::setw(2) << std::setfill('0') << (int)md_value[i]; } return ss.str(); } diff --git a/src/crypto/src/x509.cpp b/src/crypto/src/x509.cpp index 4676662f..3c80dd2a 100644 --- a/src/crypto/src/x509.cpp +++ b/src/crypto/src/x509.cpp @@ -2,6 +2,7 @@ #include #include #include +#include #include #include #include @@ -15,13 +16,20 @@ EVP_PKEY *generate_key() { throw std::runtime_error("Unable to create EVP_PKEY structure."); } - auto big_num = BN_new(); - auto rsa = RSA_new(); - BN_set_word(big_num, RSA_F4); - RSA_generate_key_ex(rsa, 2048, big_num, nullptr); - if (!EVP_PKEY_assign_RSA(pkey, rsa)) { + EVP_PKEY_CTX *ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_RSA, nullptr); + if (!ctx) { throw std::runtime_error("Unable to generate 2048-bit RSA key."); } + if (EVP_PKEY_keygen_init(ctx) <= 0) { + throw std::runtime_error("Unable to generate 2048-bit RSA key."); + } + if (EVP_PKEY_CTX_set_rsa_keygen_bits(ctx, 2048) <= 0) { + throw std::runtime_error("Unable to generate 2048-bit RSA key."); + } + if (EVP_PKEY_keygen(ctx, &pkey) <= 0) { + throw std::runtime_error("Unable to generate 2048-bit RSA key."); + } + EVP_PKEY_CTX_free(ctx); return pkey; } @@ -159,8 +167,6 @@ std::string get_cert_pem(const X509 &x509) { std::string get_key_content(EVP_PKEY *pkey, bool private_key) { BIO *bio; - BUF_MEM *bufmem; - char *pem; bio = BIO_new(BIO_s_mem()); @@ -175,7 +181,9 @@ std::string get_key_content(EVP_PKEY *pkey, bool private_key) { BIO_read(bio, key, keylen); BIO_free_all(bio); - return {key, static_cast(keylen)}; + std::string result(key, static_cast(keylen)); + free(key); + return result; } std::string get_pkey_content(EVP_PKEY *pkey) { @@ -234,12 +242,15 @@ std::optional verification_error(X509 *paired_cert, X509 *untrusted X509_STORE_CTX_set_flags(_cert_ctx, X509_V_FLAG_PARTIAL_CHAIN); auto err = X509_verify_cert(_cert_ctx); + X509_STORE_free(x509_store); if (err == 1) { + X509_STORE_CTX_free(_cert_ctx); return std::nullopt; } int err_code = X509_STORE_CTX_get_error(_cert_ctx); + X509_STORE_CTX_free(_cert_ctx); return X509_verify_cert_error_string(err_code); } diff --git a/tests/testCrypto.cpp b/tests/testCrypto.cpp index 0add12e4..f625fe10 100644 --- a/tests/testCrypto.cpp +++ b/tests/testCrypto.cpp @@ -112,38 +112,51 @@ TEST_CASE("AES cbc", "[Crypto]") { } TEST_CASE("OpenSSL sign", "[Crypto]") { - /* Generated using: `openssl genrsa -out mykey.pem 1024` */ - auto private_key = "-----BEGIN RSA PRIVATE KEY-----\n" - "MIICXAIBAAKBgQDwRIo9jwMkSxUPLbuLSnUpy4yoRkA8L1NGitnQjrol9ouz436k\n" - "7Ip0jyEqAqLdmSAJYtoyqx78BbPP1ubscrKwjWVxf67FeZBWavhkhNhZoaXWYmhN\n" - "Pif5OlvI6WFasg4L6IDGOh/Gl6SrqUntYsYLqJmvfuJf175zdS3YCribdwIDAQAB\n" - "AoGBANVI8rK8vlw8boBf54k52pH0iHNkkWcb17/aSIrz+Fj06IUS4PyEok/gMt95\n" - "IZy3bpIGd43dDA9K/Jj2u12QX//Tx96DbJznmjMeOTEJY/+Hl7rfNGEchUyBsZeP\n" - "vW5KdIHKGaoLkXnuDtsQjq63nzkPEa9Ijl7dFYtUZ9FdQvsBAkEA/YDRefFlbkXJ\n" - "CMWzD/x+s/YEbvBwW5qA9V5qaCpdRjHhdOuZ6VDUl627IktF3L4QfUPlCWJKUSeW\n" - "670X9hxggQJBAPKiWZNgzIH7VpD+mbxneGW1wjGRx5MO6AntdxINhnC8jFJcw6Jl\n" - "y+GthxoA07OuSjdU5oKfrUZTGBLzf9W3//cCQBS0ted48Sj9qDsAMu0GWa8HVDtf\n" - "hj3lM81W5egWNcIrBthO+iZVhNfSx+s4LL+oAp7Ised/UMSqMCiXLGLc1IECQEH0\n" - "nfLxEkaXIv4BJ5tOaS0EzogY/65bE/p24bI3mP8WUfKlosyHbXeoaxxHc0TZsPT/\n" - "kDWb4EdImTe1l19qSBsCQBBq6j2aIC2MMKatQ4916tGxDdfJrUKpujtqypOeVCu4\n" - "TrWeVb6zYtY5BC6Y+AzitAkHLg+gZ8df6B5z4cgATOs=\n" - "-----END RSA PRIVATE KEY-----"s; - /* Generated from the private key using: `openssl rsa -in mykey.pem -pubout > mykey.pub` */ - auto public_key = "-----BEGIN PUBLIC KEY-----\n" - "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDwRIo9jwMkSxUPLbuLSnUpy4yo\n" - "RkA8L1NGitnQjrol9ouz436k7Ip0jyEqAqLdmSAJYtoyqx78BbPP1ubscrKwjWVx\n" - "f67FeZBWavhkhNhZoaXWYmhNPif5OlvI6WFasg4L6IDGOh/Gl6SrqUntYsYLqJmv\n" - "fuJf175zdS3YCribdwIDAQAB\n" - "-----END PUBLIC KEY-----"s; - - auto msg = "A very important message to be signed"s; - auto signature = crypto::sign(msg, private_key); - - REQUIRE(crypto::str_to_hex(signature) == - "BE6EDF421CEFC1D0AFFB88487A2A23FA0B12DAABAE87D263F0F9A8F36758D30FE52EE475FFE" - "A11D00DED565406807968E8F14A8D3C1DC0E01E3D71B0AE7495232F425E1CA62F403069164A1" - "1225F18CA5472932BE34A82A78BC0A06CE7503AAD2EC7BA5A77A0A8A1D3F83623EE1D3F89EB4" - "F10B0B72642FB88CD08C055D64CFE"); - - REQUIRE(crypto::verify(msg, signature, public_key)); + SECTION("Manually created") { /* Generated using: `openssl genrsa -out mykey.pem 1024` */ + auto private_key = "-----BEGIN RSA PRIVATE KEY-----\n" + "MIICXAIBAAKBgQDwRIo9jwMkSxUPLbuLSnUpy4yoRkA8L1NGitnQjrol9ouz436k\n" + "7Ip0jyEqAqLdmSAJYtoyqx78BbPP1ubscrKwjWVxf67FeZBWavhkhNhZoaXWYmhN\n" + "Pif5OlvI6WFasg4L6IDGOh/Gl6SrqUntYsYLqJmvfuJf175zdS3YCribdwIDAQAB\n" + "AoGBANVI8rK8vlw8boBf54k52pH0iHNkkWcb17/aSIrz+Fj06IUS4PyEok/gMt95\n" + "IZy3bpIGd43dDA9K/Jj2u12QX//Tx96DbJznmjMeOTEJY/+Hl7rfNGEchUyBsZeP\n" + "vW5KdIHKGaoLkXnuDtsQjq63nzkPEa9Ijl7dFYtUZ9FdQvsBAkEA/YDRefFlbkXJ\n" + "CMWzD/x+s/YEbvBwW5qA9V5qaCpdRjHhdOuZ6VDUl627IktF3L4QfUPlCWJKUSeW\n" + "670X9hxggQJBAPKiWZNgzIH7VpD+mbxneGW1wjGRx5MO6AntdxINhnC8jFJcw6Jl\n" + "y+GthxoA07OuSjdU5oKfrUZTGBLzf9W3//cCQBS0ted48Sj9qDsAMu0GWa8HVDtf\n" + "hj3lM81W5egWNcIrBthO+iZVhNfSx+s4LL+oAp7Ised/UMSqMCiXLGLc1IECQEH0\n" + "nfLxEkaXIv4BJ5tOaS0EzogY/65bE/p24bI3mP8WUfKlosyHbXeoaxxHc0TZsPT/\n" + "kDWb4EdImTe1l19qSBsCQBBq6j2aIC2MMKatQ4916tGxDdfJrUKpujtqypOeVCu4\n" + "TrWeVb6zYtY5BC6Y+AzitAkHLg+gZ8df6B5z4cgATOs=\n" + "-----END RSA PRIVATE KEY-----"s; + /* Generated from the private key using: `openssl rsa -in mykey.pem -pubout > mykey.pub` */ + auto public_key = "-----BEGIN PUBLIC KEY-----\n" + "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDwRIo9jwMkSxUPLbuLSnUpy4yo\n" + "RkA8L1NGitnQjrol9ouz436k7Ip0jyEqAqLdmSAJYtoyqx78BbPP1ubscrKwjWVx\n" + "f67FeZBWavhkhNhZoaXWYmhNPif5OlvI6WFasg4L6IDGOh/Gl6SrqUntYsYLqJmv\n" + "fuJf175zdS3YCribdwIDAQAB\n" + "-----END PUBLIC KEY-----"s; + + auto msg = "A very important message to be signed"s; + auto signature = crypto::sign(msg, private_key); + + REQUIRE(crypto::str_to_hex(signature) == + "BE6EDF421CEFC1D0AFFB88487A2A23FA0B12DAABAE87D263F0F9A8F36758D30FE52EE475FFE" + "A11D00DED565406807968E8F14A8D3C1DC0E01E3D71B0AE7495232F425E1CA62F403069164A1" + "1225F18CA5472932BE34A82A78BC0A06CE7503AAD2EC7BA5A77A0A8A1D3F83623EE1D3F89EB4" + "F10B0B72642FB88CD08C055D64CFE"); + + REQUIRE(crypto::verify(msg, signature, public_key)); + } + SECTION("Auto generated") { + auto pkey = x509::generate_key(); + auto pcert = x509::generate_x509(pkey); + + auto private_key = x509::get_pkey_content(pkey); + auto public_cert = x509::get_cert_public_key(pcert); + + auto msg = "A very important message to be signed"s; + auto signature = crypto::sign(msg, private_key); + + REQUIRE(crypto::verify(msg, signature, public_cert)); + } } \ No newline at end of file From 77192f1719b817cee09782453e622c513b1247fe Mon Sep 17 00:00:00 2001 From: ABeltramo Date: Fri, 27 Jan 2023 23:19:45 +0000 Subject: [PATCH 23/25] fix: memory leak in the gstreamer plugins --- CMakeLists.txt | 4 + src/streaming/streaming/gst-plugin/audio.hpp | 6 +- .../gst-plugin/gstrtpmoonlightpay_audio.cpp | 2 + .../gst-plugin/gstrtpmoonlightpay_video.cpp | 2 + src/streaming/streaming/gst-plugin/utils.hpp | 19 +-- src/streaming/streaming/gst-plugin/video.hpp | 41 +++-- src/wolf/wolf.cpp | 7 +- tests/platforms/linux/input.cpp | 40 ++--- tests/testGSTPlugin.cpp | 144 ++++++++++++------ 9 files changed, 171 insertions(+), 94 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index d85d5fd2..cd3cdab6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -12,6 +12,10 @@ if (CMAKE_PROJECT_NAME STREQUAL PROJECT_NAME) # Optionally set things like CMAKE_CXX_STANDARD, CMAKE_POSITION_INDEPENDENT_CODE here + set(CMAKE_CXX_STANDARD 17) + + set(CMAKE_CXX_STANDARD_REQUIRED ON) + # Let's ensure -std=c++xx instead of -std=g++xx set(CMAKE_CXX_EXTENSIONS OFF) diff --git a/src/streaming/streaming/gst-plugin/audio.hpp b/src/streaming/streaming/gst-plugin/audio.hpp index 01783648..0ac06931 100644 --- a/src/streaming/streaming/gst-plugin/audio.hpp +++ b/src/streaming/streaming/gst-plugin/audio.hpp @@ -89,10 +89,10 @@ static GstBuffer *create_rtp_audio_buffer(const gst_rtp_moonlight_pay_audio &rtp static GstBufferList *split_into_rtp(gst_rtp_moonlight_pay_audio *rtpmoonlightpay, GstBuffer *inbuf) { bool time_to_fec = (rtpmoonlightpay->cur_seq_number + 1) % AUDIO_DATA_SHARDS == 0; - GstBufferList *rtp_packets = gst_buffer_list_new_sized(time_to_fec ? 1 + AUDIO_FEC_SHARDS : 1); + GstBufferList *rtp_packets = gst_buffer_list_new(); auto rtp_audio_buf = create_rtp_audio_buffer(*rtpmoonlightpay, inbuf); - gst_buffer_list_insert(rtp_packets, -1, rtp_audio_buf); + gst_buffer_list_add(rtp_packets, rtp_audio_buf); // save the payload locally gst_buffer_copy_into(rtp_audio_buf, @@ -118,7 +118,7 @@ static GstBufferList *split_into_rtp(gst_rtp_moonlight_pay_audio *rtpmoonlightpa auto fec_buf = gst_buffer_append(fec_packet_header, fec_payload_buf); gst_copy_timestamps(inbuf, fec_buf); - gst_buffer_list_insert(rtp_packets, -1, fec_buf); + gst_buffer_list_add(rtp_packets, fec_buf); } } rtpmoonlightpay->cur_seq_number++; diff --git a/src/streaming/streaming/gst-plugin/gstrtpmoonlightpay_audio.cpp b/src/streaming/streaming/gst-plugin/gstrtpmoonlightpay_audio.cpp index 3ee52a48..aa075e16 100644 --- a/src/streaming/streaming/gst-plugin/gstrtpmoonlightpay_audio.cpp +++ b/src/streaming/streaming/gst-plugin/gstrtpmoonlightpay_audio.cpp @@ -248,6 +248,8 @@ static GstFlowReturn gst_rtp_moonlight_pay_audio_generate_output(GstBaseTransfor /* Send the generated packets to any downstream listener */ gst_pad_push_list(trans->srcpad, rtp_packets); + gst_buffer_unref(inbuf); + /* Setting outbuf to NULL and returning GST_BASE_TRANSFORM_FLOW_DROPPED will signal that we finished doing business */ outbuf = nullptr; return GST_BASE_TRANSFORM_FLOW_DROPPED; diff --git a/src/streaming/streaming/gst-plugin/gstrtpmoonlightpay_video.cpp b/src/streaming/streaming/gst-plugin/gstrtpmoonlightpay_video.cpp index 0930ab06..d0937402 100644 --- a/src/streaming/streaming/gst-plugin/gstrtpmoonlightpay_video.cpp +++ b/src/streaming/streaming/gst-plugin/gstrtpmoonlightpay_video.cpp @@ -248,6 +248,8 @@ static GstFlowReturn gst_rtp_moonlight_pay_video_generate_output(GstBaseTransfor /* Send the generated packets to any downstream listener */ gst_pad_push_list(trans->srcpad, rtp_packets); + gst_buffer_unref(inbuf); + /* Setting outbuf to NULL and returning GST_BASE_TRANSFORM_FLOW_DROPPED will signal that we finished doing business */ outbuf = nullptr; return GST_BASE_TRANSFORM_FLOW_DROPPED; diff --git a/src/streaming/streaming/gst-plugin/utils.hpp b/src/streaming/streaming/gst-plugin/utils.hpp index 1093877f..bbc7d9ee 100644 --- a/src/streaming/streaming/gst-plugin/utils.hpp +++ b/src/streaming/streaming/gst-plugin/utils.hpp @@ -17,9 +17,7 @@ static void gst_buffer_copy_into(GstBuffer *buf, unsigned char *destination) { /* get READ access to the memory and fill with vals */ GstMapInfo info; gst_buffer_map(buf, &info, GST_MAP_READ); - for (int i = 0; i < size; i++) { - destination[i] = info.data[i]; - } + std::copy(info.data, info.data + size, destination); gst_buffer_unmap(buf, &info); } @@ -48,7 +46,7 @@ static std::vector gst_buffer_copy_content(GstBuffer *buf) { * Creates a GstBuffer and fill the memory with the given value */ static GstBuffer *gst_buffer_new_and_fill(gsize size, int fill_val) { - GstBuffer *buf = gst_buffer_new_allocate(NULL, size, NULL); + GstBuffer *buf = gst_buffer_new_allocate(nullptr, size, nullptr); /* get WRITE access to the memory and fill with fill_val */ GstMapInfo info; @@ -62,15 +60,8 @@ static GstBuffer *gst_buffer_new_and_fill(gsize size, int fill_val) { * Creates a GstBuffer from the given array of chars */ static GstBuffer *gst_buffer_new_and_fill(gsize size, const char vals[]) { - GstBuffer *buf = gst_buffer_new_allocate(NULL, size, NULL); - - /* get WRITE access to the memory and fill with vals */ - GstMapInfo info; - gst_buffer_map(buf, &info, GST_MAP_WRITE); - for (int i = 0; i < size; i++) { - info.data[i] = vals[i]; - } - gst_buffer_unmap(buf, &info); + GstBuffer *buf = gst_buffer_new_allocate(nullptr, size, nullptr); + gst_buffer_fill(buf, 0, vals, size); return buf; } @@ -95,7 +86,7 @@ static GstBuffer *gst_buffer_list_unfold(GstBufferList *buffer_list) { * No copy of the stored data is performed */ static GstBufferList *gst_buffer_list_sub(GstBufferList *buffer_list, int start, int end) { - GstBufferList *res = gst_buffer_list_new_sized(end - start); + GstBufferList *res = gst_buffer_list_new(); for (int idx = start; idx < end; idx++) { auto buf_idx = diff --git a/src/streaming/streaming/gst-plugin/video.hpp b/src/streaming/streaming/gst-plugin/video.hpp index a54de16b..b47ae677 100644 --- a/src/streaming/streaming/gst-plugin/video.hpp +++ b/src/streaming/streaming/gst-plugin/video.hpp @@ -1,5 +1,6 @@ #pragma once #include +#include #include #include @@ -53,8 +54,7 @@ create_rtp_header(const gst_rtp_moonlight_pay_video &rtpmoonlightpay, int packet static GstBuffer *prepend_video_header(GstBuffer *inbuf) { constexpr auto video_payload_header_size = 8; GstBuffer *video_header = gst_buffer_new_and_fill(video_payload_header_size, "\0017charss"); - auto full_payload_buf = gst_buffer_append(video_header, inbuf); - gst_copy_timestamps(inbuf, full_payload_buf); + auto full_payload_buf = gst_buffer_append(video_header, gst_buffer_ref(inbuf)); return full_payload_buf; } @@ -65,25 +65,25 @@ static GstBufferList *generate_rtp_packets(const gst_rtp_moonlight_pay_video &rt auto in_buf_size = gst_buffer_get_size(inbuf); auto payload_size = rtpmoonlightpay.payload_size - MAX_RTP_HEADER_SIZE; auto tot_packets = std::ceil((float)in_buf_size / payload_size); - GstBufferList *buffers = gst_buffer_list_new_sized(tot_packets); + GstBufferList *buffers = gst_buffer_list_new(); for (int packet_nr = 0; packet_nr < tot_packets; packet_nr++) { auto begin = packet_nr * payload_size; auto remaining = in_buf_size - begin; auto packet_payload_size = MIN(remaining, payload_size); - GstBuffer *header = create_rtp_header(rtpmoonlightpay, packet_nr, tot_packets); + GstBuffer *rtp_packet = create_rtp_header(rtpmoonlightpay, packet_nr, tot_packets); GstBuffer *payload = gst_buffer_copy_region(inbuf, GST_BUFFER_COPY_ALL, begin, packet_payload_size); - GstBuffer *rtp_packet = gst_buffer_append(header, payload); + rtp_packet = gst_buffer_append(rtp_packet, payload); if ((remaining < payload_size) && rtpmoonlightpay.add_padding) { GstBuffer *padding = gst_buffer_new_and_fill(payload_size - remaining, 0x00); - gst_buffer_append(rtp_packet, padding); + rtp_packet = gst_buffer_append(rtp_packet, padding); } gst_copy_timestamps(inbuf, rtp_packet); - gst_buffer_list_insert(buffers, -1, rtp_packet); + gst_buffer_list_add(buffers, rtp_packet); } return buffers; @@ -149,6 +149,15 @@ static void generate_fec_packets(const gst_rtp_moonlight_pay_video &rtpmoonlight auto blocks = determine_split(rtpmoonlightpay, gst_buffer_list_length(rtp_packets)); auto nr_shards = blocks.data_shards + blocks.parity_shards; + if (nr_shards > DATA_SHARDS_MAX) { + logs::log(logs::warning, + "[GSTREAMER] Size of frame too large, {} packets is bigger than the max ({}); skipping FEC", + nr_shards, + DATA_SHARDS_MAX); + gst_buffer_unref(rtp_payload); + return; + } + // pads rtp_payload to blocksize if (payload_size % blocks.block_size != 0) { GstBuffer *pad = gst_buffer_new_and_fill((blocks.data_shards * blocks.block_size) - payload_size, 0x00); @@ -156,8 +165,8 @@ static void generate_fec_packets(const gst_rtp_moonlight_pay_video &rtpmoonlight } // Allocate space for FEC packets - rtp_payload = - gst_buffer_append(rtp_payload, gst_buffer_new_and_fill((blocks.parity_shards * blocks.block_size), 0x00)); + auto fec_buff = gst_buffer_new_and_fill((blocks.parity_shards * blocks.block_size), 0x00); + rtp_payload = gst_buffer_append(rtp_payload, fec_buff); gst_buffer_map(rtp_payload, &info, GST_MAP_WRITE); // Reed Solomon encode the full stream of bytes @@ -201,10 +210,11 @@ static void generate_fec_packets(const gst_rtp_moonlight_pay_video &rtpmoonlight GstBuffer *packet_buf = gst_buffer_new_allocate(nullptr, blocks.block_size, nullptr); gst_buffer_fill(packet_buf, 0, rtp_packet, blocks.block_size); - gst_buffer_list_insert(rtp_packets, -1, packet_buf); + gst_buffer_list_add(rtp_packets, packet_buf); } gst_buffer_unmap(rtp_payload, &info); + gst_buffer_unref(rtp_payload); } /** @@ -221,7 +231,7 @@ generate_fec_multi_blocks(gst_rtp_moonlight_pay_video *rtpmoonlightpay, GstBuffe constexpr auto nr_blocks = 3; constexpr auto last_block_index = 2 << 6; - GstBufferList *final_packets = gst_buffer_list_new_sized(0); // we'll increase the size on each block iteration + GstBufferList *final_packets = gst_buffer_list_new(); // we'll increase the size on each block iteration auto packets_per_block = (int)std::ceil((float)data_shards / nr_blocks); for (int block_idx = 0; block_idx < nr_blocks; block_idx++) { @@ -237,13 +247,16 @@ generate_fec_multi_blocks(gst_rtp_moonlight_pay_video *rtpmoonlightpay, GstBuffe auto total_block_packets = gst_buffer_list_length(block_packets); for (int packet_idx = 0; packet_idx < total_block_packets; packet_idx++) { // copy here is about the buffer object, not the data - gst_buffer_list_insert(final_packets, -1, gst_buffer_copy(gst_buffer_list_get(block_packets, packet_idx))); + gst_buffer_list_add(final_packets, gst_buffer_copy(gst_buffer_list_get(block_packets, packet_idx))); } // This will adjust the sequenceNumber of the RTP packet rtpmoonlightpay->cur_seq_number += total_block_packets; + gst_buffer_list_unref(block_packets); } + gst_buffer_list_unref(rtp_packets); + return final_packets; } @@ -258,8 +271,9 @@ static GstBufferList *split_into_rtp(gst_rtp_moonlight_pay_video *rtpmoonlightpa auto full_payload_buf = prepend_video_header(inbuf); GstBufferList *rtp_packets = generate_rtp_packets(*rtpmoonlightpay, full_payload_buf); - auto rtp_packets_size = gst_buffer_list_length(rtp_packets); + if (rtpmoonlightpay->fec_percentage > 0) { + auto rtp_packets_size = gst_buffer_list_length(rtp_packets); auto blocks = determine_split(*rtpmoonlightpay, rtp_packets_size); // With a fec_percentage of 255, if payload is broken up into more than a 100 data_shards @@ -273,6 +287,7 @@ static GstBufferList *split_into_rtp(gst_rtp_moonlight_pay_video *rtpmoonlightpa } rtpmoonlightpay->frame_num++; + gst_buffer_unref(full_payload_buf); return rtp_packets; } diff --git a/src/wolf/wolf.cpp b/src/wolf/wolf.cpp index 6ea3a611..cda02544 100644 --- a/src/wolf/wolf.cpp +++ b/src/wolf/wolf.cpp @@ -242,7 +242,7 @@ int main(int argc, char *argv[]) { auto sess_handlers = setup_sessions_handlers(local_state->event_bus, threads); // Exception and termination handling - shutdown_handler = [&sess_handlers](int signum) { + shutdown_handler = [&sess_handlers, &threads](int signum) { logs::log(logs::info, "Received interrupt signal {}, clean exit", signum); if (signum == SIGABRT || signum == SIGSEGV) { auto trace_file = "./backtrace.dump"; @@ -250,10 +250,15 @@ int main(int argc, char *argv[]) { boost::stacktrace::safe_dump_to(trace_file); } + for(const auto &thread_pool : *threads.load()){ + thread_pool.second->stop(); + } + for (const auto &handler : sess_handlers) { handler->unregister(); } + logs::log(logs::info, "See ya!"); exit(signum); }; std::signal(SIGINT, signal_handler); diff --git a/tests/platforms/linux/input.cpp b/tests/platforms/linux/input.cpp index 2cd56efc..43a6bc9c 100644 --- a/tests/platforms/linux/input.cpp +++ b/tests/platforms/linux/input.cpp @@ -122,26 +122,26 @@ TEST_CASE("uinput - mouse", "UINPUT") { } TEST_CASE("uinput - touchpad", "UINPUT") { - libevdev_ptr mouse_abs(libevdev_new(), ::libevdev_free); - libevdev_uinput_ptr touch_el = {mouse::create_mouse_abs(mouse_abs.get()).value(), ::libevdev_uinput_destroy}; - struct input_event ev {}; - - link_devnode(mouse_abs.get(), touch_el.get()); - - auto rc = libevdev_next_event(mouse_abs.get(), LIBEVDEV_READ_FLAG_NORMAL, &ev); - REQUIRE(rc == -EAGAIN); - - auto mv_packet = data::MOUSE_MOVE_ABS_PACKET{.x = boost::endian::native_to_big((short)10), - .y = boost::endian::native_to_big((short)20), - .width = boost::endian::native_to_big((short)1920), - .height = boost::endian::native_to_big((short)1080)}; - mouse::move_mouse_abs(touch_el.get(), mv_packet); - - rc = libevdev_next_event(mouse_abs.get(), LIBEVDEV_READ_FLAG_NORMAL, &ev); - REQUIRE(rc == LIBEVDEV_READ_STATUS_SUCCESS); - REQUIRE_THAT(libevdev_event_type_get_name(ev.type), Equals("EV_ABS")); - REQUIRE_THAT(libevdev_event_code_get_name(ev.type, ev.code), Equals("ABS_X")); - REQUIRE(ev.value == 10); + // libevdev_ptr mouse_abs(libevdev_new(), ::libevdev_free); + // libevdev_uinput_ptr touch_el = {mouse::create_mouse_abs(mouse_abs.get()).value(), ::libevdev_uinput_destroy}; + // struct input_event ev {}; + // + // link_devnode(mouse_abs.get(), touch_el.get()); + // + // auto rc = libevdev_next_event(mouse_abs.get(), LIBEVDEV_READ_FLAG_NORMAL, &ev); + // REQUIRE(rc == -EAGAIN); + // + // auto mv_packet = data::MOUSE_MOVE_ABS_PACKET{.x = boost::endian::native_to_big((short)10), + // .y = boost::endian::native_to_big((short)20), + // .width = boost::endian::native_to_big((short)1920), + // .height = boost::endian::native_to_big((short)1080)}; + // mouse::move_mouse_abs(touch_el.get(), mv_packet); + // + // rc = libevdev_next_event(mouse_abs.get(), LIBEVDEV_READ_FLAG_NORMAL, &ev); + // REQUIRE(rc == LIBEVDEV_READ_STATUS_SUCCESS); + // REQUIRE_THAT(libevdev_event_type_get_name(ev.type), Equals("EV_ABS")); + // REQUIRE_THAT(libevdev_event_code_get_name(ev.type, ev.code), Equals("ABS_X")); + // REQUIRE(ev.value == 10); // TODO: why are the followings not reported? diff --git a/tests/testGSTPlugin.cpp b/tests/testGSTPlugin.cpp index 0edd6936..d75e6498 100644 --- a/tests/testGSTPlugin.cpp +++ b/tests/testGSTPlugin.cpp @@ -12,22 +12,29 @@ using namespace std::string_literals; /* UTILS */ -static state::VideoRTPHeaders *get_rtp_video_from_buf(GstBuffer *buf) { +static std::pair copy_buffer_data(GstBuffer *buf) { + auto size = gst_buffer_get_size(buf); + auto *res = new char[size]; + GstMapInfo info; gst_buffer_map(buf, &info, GST_MAP_READ); - return (state::VideoRTPHeaders *)info.data; + std::copy(info.data, info.data + size, res); + gst_buffer_unmap(buf, &info); + + return {res, size}; } static state::AudioRTPHeaders *get_rtp_audio_from_buf(GstBuffer *buf) { - GstMapInfo info; - gst_buffer_map(buf, &info, GST_MAP_READ); - return (state::AudioRTPHeaders *)info.data; + return (state::AudioRTPHeaders *)copy_buffer_data(buf).first; } -static char *get_c_str_from_buf(GstBuffer *buf) { - GstMapInfo info; - gst_buffer_map(buf, &info, GST_MAP_WRITE); - return (char *)info.data; +static std::string get_str_from_buf(GstBuffer *buf) { + auto copy = copy_buffer_data(buf); + return {copy.first, copy.second}; +} + +static int get_buf_refcount(GstBuffer *buf) { + return buf->mini_object.refcount; } class GStreamerTestsFixture { @@ -46,6 +53,8 @@ TEST_CASE_METHOD(GStreamerTestsFixture, "Basic utils", "[GSTPlugin]") { auto buffer = gst_buffer_new_and_fill(10, 0); REQUIRE_THAT(gst_buffer_copy_content(buffer), Catch::Matchers::SizeIs(10) && Equals(std::vector{0, 0, 0, 0, 0, 0, 0, 0, 0, 0})); + /* Cleanup */ + REQUIRE(get_buf_refcount(buffer) == 1); gst_buffer_unref(buffer); auto payload = "char array"s; @@ -53,6 +62,10 @@ TEST_CASE_METHOD(GStreamerTestsFixture, "Basic utils", "[GSTPlugin]") { REQUIRE_THAT( gst_buffer_copy_content(buffer), Catch::Matchers::SizeIs(payload.size()) && Equals(std::vector(payload.begin(), payload.end()))); + REQUIRE_THAT(get_str_from_buf(buffer), Catch::Matchers::SizeIs(payload.size()) && Equals(payload)); + + /* Cleanup */ + REQUIRE(get_buf_refcount(buffer) == 1); gst_buffer_unref(buffer); } @@ -67,11 +80,15 @@ TEST_CASE_METHOD(GStreamerTestsFixture, "Encrypt GstBuffer", "[GSTPlugin]") { REQUIRE_THAT(iv_str, Equals("\000\274aN\000\000\000\000\000\000\000\000\000\000\000\000"s)); auto encrypted = encrypt_payload(aes_key, iv_str, payload); - std::string encrypted_str(get_c_str_from_buf(encrypted), gst_buffer_get_size(encrypted)); + auto encrypted_str = get_str_from_buf(encrypted); auto decrypted = crypto::aes_decrypt_cbc(encrypted_str, aes_key, iv_str, true); REQUIRE_THAT(gst_buffer_copy_content(payload), Equals(std::vector(decrypted.begin(), decrypted.end()))); + + /* Cleanup */ + REQUIRE(get_buf_refcount(payload) == 1); + gst_buffer_unref(payload); } /* @@ -94,7 +111,6 @@ TEST_CASE_METHOD(GStreamerTestsFixture, "RTP VIDEO Splits", "[GSTPlugin]") { rtpmoonlightpay->add_padding = false; auto payload_buf = gst_buffer_new_and_fill(payload_str.size(), payload_str.c_str()); - auto rtp_packets = gst_moonlight_video::split_into_rtp(rtpmoonlightpay, payload_buf); auto payload_expected_packets = std::ceil((payload_str.size() + rtp_payload_header_size) / @@ -123,6 +139,12 @@ TEST_CASE_METHOD(GStreamerTestsFixture, "RTP VIDEO Splits", "[GSTPlugin]") { REQUIRE_THAT(std::string(first_payload.begin(), first_payload.end()), Equals("\0017charssNever go")); // TODO: proper check content and FEC } + + /* Cleanup */ + REQUIRE(GST_OBJECT_REFCOUNT(rtpmoonlightpay) == 1); + g_object_unref(rtpmoonlightpay); + REQUIRE(get_buf_refcount(payload_buf) == 1); + g_object_unref(payload_buf); } TEST_CASE_METHOD(GStreamerTestsFixture, "Create RTP VIDEO packets", "[GSTPlugin]") { @@ -131,6 +153,8 @@ TEST_CASE_METHOD(GStreamerTestsFixture, "Create RTP VIDEO packets", "[GSTPlugin] rtpmoonlightpay->payload_size = 10 + MAX_RTP_HEADER_SIZE; // This will include 8bytes of payload header (017charss) rtpmoonlightpay->fec_percentage = 50; rtpmoonlightpay->add_padding = true; + auto rtp_packet_size = rtpmoonlightpay->payload_size + sizeof(moonlight::NV_VIDEO_PACKET); + auto rtp_header_size = (long)sizeof(state::VideoRTPHeaders); auto payload = gst_buffer_new_and_fill(10, "$A PAYLOAD"); auto video_payload = gst_moonlight_video::prepend_video_header(payload); @@ -138,38 +162,33 @@ TEST_CASE_METHOD(GStreamerTestsFixture, "Create RTP VIDEO packets", "[GSTPlugin] // 10 bytes of actual payload + 8 bytes of payload header // will be splitted in two RTP packets + REQUIRE(gst_buffer_get_size(video_payload) == gst_buffer_get_size(payload) + 8); // Added 017charss REQUIRE(gst_buffer_list_length(rtp_packets) == 2); SECTION("First packet") { - auto buf = gst_buffer_list_get(rtp_packets, 0); - auto rtp_packet = get_rtp_video_from_buf(buf); + auto first_packet = gst_buffer_copy_content(gst_buffer_list_get(rtp_packets, 0)); + auto rtp_packet = reinterpret_cast(first_packet.data()); REQUIRE(rtp_packet->packet.flags == FLAG_CONTAINS_PIC_DATA + FLAG_SOF); REQUIRE(rtp_packet->packet.frameIndex == 0); REQUIRE(rtp_packet->packet.streamPacketIndex == 0); REQUIRE(rtp_packet->rtp.sequenceNumber == boost::endian::native_to_big((uint16_t)0)); - auto rtp_payload = gst_buffer_copy_content(buf, sizeof(state::VideoRTPHeaders)); - auto expected_result = "\0017charss$A"s; - REQUIRE_THAT(rtp_payload, - Catch::Matchers::SizeIs(10) && - Equals(std::vector(expected_result.begin(), expected_result.end()))); + auto rtp_payload = std::string(first_packet.begin() + rtp_header_size, first_packet.end()); + REQUIRE_THAT("\0017charss$A"s, Equals(rtp_payload)); } SECTION("Second packet") { - auto buf = gst_buffer_list_get(rtp_packets, 1); - auto rtp_packet = get_rtp_video_from_buf(buf); + auto second_packet = gst_buffer_copy_content(gst_buffer_list_get(rtp_packets, 1)); + auto rtp_packet = reinterpret_cast(second_packet.data()); REQUIRE(rtp_packet->packet.flags == FLAG_CONTAINS_PIC_DATA + FLAG_EOF); REQUIRE(rtp_packet->packet.frameIndex == 0); REQUIRE(rtp_packet->packet.streamPacketIndex == 0x100); REQUIRE(rtp_packet->rtp.sequenceNumber == boost::endian::native_to_big((uint16_t)1)); - auto rtp_payload = gst_buffer_copy_content(buf, sizeof(state::VideoRTPHeaders)); - auto expected_result = " PAYLOAD\0\0"s; // Will contain extra 0x0 padding at the end - REQUIRE_THAT(rtp_payload, - Catch::Matchers::SizeIs(10) && - Equals(std::vector(expected_result.begin(), expected_result.end()))); + auto rtp_payload = std::string(second_packet.begin() + rtp_header_size, second_packet.end()); + REQUIRE_THAT(" PAYLOAD\0\0"s, Equals(rtp_payload)); } SECTION("FEC") { @@ -178,37 +197,66 @@ TEST_CASE_METHOD(GStreamerTestsFixture, "Create RTP VIDEO packets", "[GSTPlugin] REQUIRE(gst_buffer_list_length(rtp_packets) == 4); SECTION("First packet (payload)") { - auto buf = gst_buffer_list_get(rtp_packets, 0); - auto original = gst_buffer_list_get(rtp_packets, 0); + auto first_packet = gst_buffer_copy_content(gst_buffer_list_get(rtp_packets, 0)); - // Is the RTP Header left untouched? - REQUIRE_THAT(gst_buffer_copy_content(original, 0, sizeof(moonlight::RTP_PACKET)), - Equals(gst_buffer_copy_content(buf, 0, sizeof(moonlight::RTP_PACKET)))); + auto rtp_packet = reinterpret_cast(first_packet.data()); - // Is the payload left untouched? - REQUIRE_THAT(gst_buffer_copy_content(original, sizeof(state::VideoRTPHeaders)), - Equals(gst_buffer_copy_content(buf, sizeof(state::VideoRTPHeaders)))); + REQUIRE(rtp_packet->packet.flags == FLAG_CONTAINS_PIC_DATA + FLAG_SOF); + REQUIRE(rtp_packet->packet.frameIndex == 0); + REQUIRE(rtp_packet->packet.streamPacketIndex == 0); + REQUIRE(rtp_packet->rtp.sequenceNumber == boost::endian::native_to_big((uint16_t)0)); + + // FEC additional info + REQUIRE(rtp_packet->packet.fecInfo == 8390208); + REQUIRE(rtp_packet->packet.multiFecBlocks == 0); + REQUIRE(rtp_packet->packet.multiFecFlags == 0x10); + + auto rtp_payload = std::string(first_packet.begin() + rtp_header_size, first_packet.end()); + REQUIRE_THAT("\0017charss$A"s, Equals(rtp_payload)); } SECTION("Second packet (payload)") { - auto buf = gst_buffer_list_get(rtp_packets, 1); - auto original = gst_buffer_list_get(rtp_packets, 1); + auto second_packet = gst_buffer_copy_content(gst_buffer_list_get(rtp_packets, 1)); + auto rtp_packet = reinterpret_cast(second_packet.data()); + + REQUIRE(rtp_packet->packet.flags == FLAG_CONTAINS_PIC_DATA + FLAG_EOF); + REQUIRE(rtp_packet->packet.frameIndex == 0); + REQUIRE(rtp_packet->packet.streamPacketIndex == 0x100); + REQUIRE(rtp_packet->rtp.sequenceNumber == boost::endian::native_to_big((uint16_t)1)); - // Is the RTP Header left untouched? - REQUIRE_THAT(gst_buffer_copy_content(original, 0, sizeof(moonlight::RTP_PACKET)), - Equals(gst_buffer_copy_content(buf, 0, sizeof(moonlight::RTP_PACKET)))); + // FEC additional info + REQUIRE(rtp_packet->packet.fecInfo == 8394304); + REQUIRE(rtp_packet->packet.multiFecBlocks == 0); + REQUIRE(rtp_packet->packet.multiFecFlags == 0x10); - // Is the payload left untouched? - REQUIRE_THAT(gst_buffer_copy_content(original, sizeof(state::VideoRTPHeaders)), - Equals(gst_buffer_copy_content(buf, sizeof(state::VideoRTPHeaders)))); + auto rtp_payload = std::string(second_packet.begin() + rtp_header_size, second_packet.end()); + REQUIRE_THAT(" PAYLOAD\0\0"s, Equals(rtp_payload)); } SECTION("Third packet (FEC)") { - auto buf = gst_buffer_list_get(rtp_packets, 2); - auto rtp_packet = get_rtp_video_from_buf(buf); + auto third_packet = gst_buffer_copy_content(gst_buffer_list_get(rtp_packets, 2)); + auto rtp_packet = reinterpret_cast(third_packet.data()); REQUIRE(rtp_packet->packet.frameIndex == 0); REQUIRE(rtp_packet->rtp.sequenceNumber == boost::endian::native_to_big((uint16_t)2)); + + // FEC additional info + REQUIRE(rtp_packet->packet.fecInfo == 8398400); + REQUIRE(rtp_packet->packet.multiFecBlocks == 0); + REQUIRE(rtp_packet->packet.multiFecFlags == 0x10); + } + + SECTION("Fourth packet (FEC)") { + auto fourth_packet = gst_buffer_copy_content(gst_buffer_list_get(rtp_packets, 3)); + auto rtp_packet = reinterpret_cast(fourth_packet.data()); + + REQUIRE(rtp_packet->packet.frameIndex == 0); + REQUIRE(rtp_packet->rtp.sequenceNumber == boost::endian::native_to_big((uint16_t)3)); + + // FEC additional info + REQUIRE(rtp_packet->packet.fecInfo == 8402496); + REQUIRE(rtp_packet->packet.multiFecBlocks == 0); + REQUIRE(rtp_packet->packet.multiFecFlags == 0x10); } SECTION("REED SOLOMON") { @@ -259,6 +307,14 @@ TEST_CASE_METHOD(GStreamerTestsFixture, "Create RTP VIDEO packets", "[GSTPlugin] } } } + + REQUIRE(GST_OBJECT_REFCOUNT(rtpmoonlightpay) == 1); + g_object_unref(rtpmoonlightpay); + REQUIRE(get_buf_refcount(payload) == 1); + g_object_unref(payload); + g_object_unref(rtp_packets); + REQUIRE(get_buf_refcount(video_payload) == 1); + g_object_unref(video_payload); } /* @@ -362,5 +418,7 @@ TEST_CASE_METHOD(GStreamerTestsFixture, "Audio RTP packet creation", "[GSTPlugin // see: https://github.com/games-on-whales/wolf/actions/runs/3553743568/jobs/5969436029 // REQUIRE_THAT(missing_pkt, Equals(original_pkt)); } + + g_object_unref(rtpmoonlightpay); } } From c8d39e01d6237d41a16ebd5e51ba2e6fe5fc5fc7 Mon Sep 17 00:00:00 2001 From: ABeltramo Date: Sat, 28 Jan 2023 11:06:25 +0000 Subject: [PATCH 24/25] fix: adding missing runtime libraries --- docker/gstreamer.Dockerfile | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/docker/gstreamer.Dockerfile b/docker/gstreamer.Dockerfile index 95606053..6653da72 100644 --- a/docker/gstreamer.Dockerfile +++ b/docker/gstreamer.Dockerfile @@ -82,10 +82,24 @@ ENV DEBIAN_FRONTEND=noninteractive RUN apt-get update -y && \ apt-get install -y --no-install-recommends \ libgstreamer1.0-0 \ - libcairo-gobject2 libgdk-pixbuf-2.0-0 libva2 libva-x11-2 libva-drm2 libxdamage1 liblcms2-2 \ + libcairo-gobject2 libgdk-pixbuf-2.0-0 libva2 libva-x11-2 libva-drm2 libxdamage1 liblcms2-2 libsoup2.4-1 \ libopenexr25 libzbar0 libopenjp2-7 librsvg2-2 libx265-199 libzxingcore1 libopenh264-6 libpulse0 \ && rm -rf /var/lib/apt/lists/* +# Adding missing libnvrtc.so for Nvidia +RUN apt-get update -y && \ + apt-get install -y wget gnupg2 && \ + apt-key del 7fa2af80 && \ + wget https://developer.download.nvidia.com/compute/cuda/repos/ubuntu2204/x86_64/cuda-keyring_1.0-1_all.deb && \ + dpkg -i cuda-keyring_1.0-1_all.deb && \ + rm cuda-keyring_1.0-1_all.deb && \ + apt-get update -y && \ + apt install -y \ + cuda-nvrtc-dev-12-0 && \ + echo "/usr/local/cuda-12.0/targets/x86_64-linux/lib/" > /etc/ld.so.conf.d/cuda.conf && \ + apt-get purge -y wget gnupg2 && \ + rm -rf /var/lib/apt/lists/* + COPY --from=builder /gstreamer/gstreamer/build/subprojects/ /gstreamer/ ENV GST_PLUGIN_PATH=/gstreamer/ From 69a9f92d8159dcb6dce4d334fb63fe3847aedce7 Mon Sep 17 00:00:00 2001 From: ABeltramo Date: Mon, 30 Jan 2023 21:00:21 +0000 Subject: [PATCH 25/25] feat: better default gstreamer pipelines --- src/streaming/streaming/streaming.cpp | 3 +- src/wolf/state/configTOML.cpp | 54 +++++++++++++++++++++------ src/wolf/state/data-structures.hpp | 28 +++++++------- tests/testMoonlight.cpp | 36 +++++++++++++++--- 4 files changed, 88 insertions(+), 33 deletions(-) diff --git a/src/streaming/streaming/streaming.cpp b/src/streaming/streaming/streaming.cpp index a5b440f6..957cd322 100644 --- a/src/streaming/streaming/streaming.cpp +++ b/src/streaming/streaming/streaming.cpp @@ -207,8 +207,7 @@ void start_streaming_audio(immer::box audio_session, unsign fmt::arg("aes_iv", audio_session->aes_iv), fmt::arg("encrypt", audio_session->encrypt_audio), fmt::arg("client_port", client_port), - fmt::arg("client_ip", audio_session->client_ip), - fmt::arg("stream_type", "audio")); + fmt::arg("client_ip", audio_session->client_ip)); run_pipeline(pipeline, [audio_session](auto pipeline, auto loop) { auto terminate_handler = audio_session->event_bus->register_handler>( diff --git a/src/wolf/state/configTOML.cpp b/src/wolf/state/configTOML.cpp index 63799bf5..eaacaff9 100644 --- a/src/wolf/state/configTOML.cpp +++ b/src/wolf/state/configTOML.cpp @@ -54,22 +54,25 @@ Config load_or_default(const std::string &source) { { logs::log(logs::warning, "Unable to open config file: {}, creating one using defaults", source); - auto video_test = "videotestsrc pattern=ball is-live=true"; - auto x11_src = "ximagesrc show-pointer=true use-damage=false"; + auto video_test = video::DEFAULT_SOURCE; + auto x11_src = "ximagesrc show-pointer=true use-damage=false ! video/x-raw, framerate={fps}/1"; auto pulse_src = "pulsesrc"; - auto default_app = toml::value{{"title", "Test ball"}, {"video", {{"source", video_test}}}}; - auto x11_sw = - toml::value{{"title", "X11 (SW)"}, {"video", {{"source", x11_src}}}, {"audio", {{"source", pulse_src}}}}; + auto default_app = toml::value{{"title", "Test ball (auto)"}, {"video", {{"source", video_test}}}}; + auto x11_auto = + toml::value{{"title", "X11 (auto)"}, {"video", {{"source", x11_src}}}, {"audio", {{"source", pulse_src}}}}; + + /* VAAPI specific encoder */ auto h264_vaapi = "vaapih264enc max-bframes=0 refs=1 num-slices={slices_per_frame} bitrate={bitrate} ! " - "video/x-h264, profile=high, stream-format=byte-stream"; + "h264parse ! " + "video/x-h264, profile=main, stream-format=byte-stream"; auto hevc_vaapi = "vaapih265enc max-bframes=0 refs=1 num-slices={slices_per_frame} bitrate={bitrate} ! " + "h265parse ! " "video/x-h265, profile=main, stream-format=byte-stream"; - auto video_vaapi = "videoscale ! videoconvert ! videorate ! " - "video/x-raw, framerate={fps}/1, chroma-site={color_range}, width={width}, height={height}, " - "format=NV12, colorimetry={color_space} ! " - "vaapipostproc"; + auto video_vaapi = "vaapipostproc ! " + "video/x-raw(memory:VASurface), chroma-site={color_range}, width={width}, " + "height={height}, format=NV12, colorimetry={color_space}"; auto test_vaapi = toml::value{{"title", "Test ball (VAAPI)"}, {"video", @@ -85,11 +88,40 @@ Config load_or_default(const std::string &source) { {"video_params", video_vaapi}}}, {"audio", {{"source", pulse_src}}}}; + /* CUDA specific encoder */ + // TODO: gop-size here should be -1 but it's not playing with Moonlight + auto h264_cuda = "nvh264enc preset=low-latency-hq zerolatency=true gop-size=0 bitrate={bitrate} aud=false ! " + "h264parse ! " + "video/x-h264, profile=main, stream-format=byte-stream"; + auto hevc_cuda = "nvh265enc preset=low-latency-hq zerolatency=true bitrate={bitrate} aud=false ! " + "h265parse ! " + "video/x-h265, profile=main, stream-format=byte-stream"; + auto video_cuda = " queue !" + " cudaupload !" + " cudascale ! " + " cudaconvert ! " + " video/x-raw(memory:CUDAMemory), width={width}, height={height}, " + " chroma-site={color_range}, format=NV12, colorimetry={color_space}"; + + auto test_cuda = toml::value{{"title", "Test ball (CUDA)"}, + {"video", + {{"source", video_test}, + {"h264_encoder", h264_cuda}, + {"hevc_encoder", hevc_cuda}, + {"video_params", video_cuda}}}}; + auto x11_cuda = toml::value{{"title", "X11 (CUDA)"}, + {"video", + {{"source", x11_src}, + {"h264_encoder", h264_cuda}, + {"hevc_encoder", hevc_cuda}, + {"video_params", video_cuda}}}, + {"audio", {{"source", pulse_src}}}}; + const toml::value data = {{"uuid", gen_uuid()}, {"hostname", "Wolf"}, {"support_hevc", true}, {"paired_clients", toml::array{}}, - {"apps", {default_app, x11_sw, test_vaapi, x11_vaapi}}, + {"apps", {default_app, x11_auto, test_vaapi, x11_vaapi, test_cuda, x11_cuda}}, {"gstreamer", // key { // array { diff --git a/src/wolf/state/data-structures.hpp b/src/wolf/state/data-structures.hpp index 3aef1e19..ad3d5d4d 100644 --- a/src/wolf/state/data-structures.hpp +++ b/src/wolf/state/data-structures.hpp @@ -50,22 +50,23 @@ using PairedClientList = immer::vector>; namespace gstreamer { namespace video { -constexpr std::string_view DEFAULT_SOURCE = "videotestsrc pattern=ball is-live=true"; -constexpr std::string_view DEFAULT_PARAMS = "videoscale ! videoconvert ! videorate ! " - "video/x-raw, width={width}, height={height}, framerate={fps}/1," - "format=I420, chroma-site={color_range}, colorimetry={color_space}"; +constexpr std::string_view DEFAULT_SOURCE = + "videotestsrc pattern=ball flip=true is-live=true ! video/x-raw, framerate={fps}/1"; +constexpr std::string_view DEFAULT_PARAMS = "videoscale ! videoconvert ! " + "video/x-raw, width={width}, height={height}, " + "chroma-site={color_range}, colorimetry={color_space}, format=NV12"; constexpr std::string_view DEFAULT_H264_ENCODER = - "x264enc pass=qual tune=zerolatency speed-preset=superfast b-adapt=false bframes=0 ref=1 bitrate={bitrate} " - "aud=false sliced-threads=true threads={slices_per_frame} " - "option-string=\"slices={slices_per_frame}:keyint=infinite:open-gop=0\" ! " - "video/x-h264, profile=high, stream-format=byte-stream"; + "encodebin " + " profile=\"video/x-h264, " + " profile=main, tune=zerolatency, bframes=0, aud=false, stream-format=byte-stream, bitrate={bitrate}, " + " insert-vui=false \""; constexpr std::string_view DEFAULT_H265_ENCODER = - "x265enc tune=zerolatency speed-preset=superfast bitrate={bitrate} " - "option-string=\"info=0:keyint=-1:qp=28:repeat-headers=1:slices={slices_per_frame}:frame-threads={slices_per_" - "frame}:aud=0:annexb=1:log-level=3:open-gop=0:bframes=0:intra-refresh=0\" ! " - "video/x-h265, profile=main, stream-format=byte-stream"; + "encodebin " + " profile=\"video/x-h265, " + " profile=main, tune=zerolatency, bframes=0, aud=false, stream-format=byte-stream, bitrate={bitrate}, " + " insert-vui=false\""; constexpr std::string_view DEFAULT_SINK = "rtpmoonlightpay_video name=moonlight_pay payload_size={payload_size} fec_percentage={fec_percentage} " @@ -76,8 +77,7 @@ constexpr std::string_view DEFAULT_SINK = namespace audio { constexpr std::string_view DEFAULT_SOURCE = "audiotestsrc wave=ticks is-live=true"; -constexpr std::string_view DEFAULT_PARAMS = "audioconvert ! audiorate ! audioresample ! " - "audio/x-raw, channels={channels}"; +constexpr std::string_view DEFAULT_PARAMS = "audio/x-raw, channels={channels}"; constexpr std::string_view DEFAULT_OPUS_ENCODER = "opusenc bitrate={bitrate} bitrate-type=cbr frame-size={packet_duration} " "bandwidth=fullband audio-type=generic max-payload-size=1400"; diff --git a/tests/testMoonlight.cpp b/tests/testMoonlight.cpp index 7f90605c..9015357d 100644 --- a/tests/testMoonlight.cpp +++ b/tests/testMoonlight.cpp @@ -24,12 +24,36 @@ TEST_CASE("LocalState load TOML", "[LocalState]") { auto app = state.apps[0]; REQUIRE_THAT(app.base.title, Equals("Ball")); REQUIRE_THAT(app.base.id, Equals("1")); - REQUIRE_THAT(app.h264_gst_pipeline, - Equals(video::DEFAULT_SOURCE.data() + " ! "s + video::DEFAULT_PARAMS.data() + " ! "s + - video::DEFAULT_H264_ENCODER.data() + " ! " + video::DEFAULT_SINK.data())); - REQUIRE_THAT(app.hevc_gst_pipeline, - Equals(video::DEFAULT_SOURCE.data() + " ! "s + video::DEFAULT_PARAMS.data() + " ! "s + - video::DEFAULT_H265_ENCODER.data() + " ! " + video::DEFAULT_SINK.data())); + REQUIRE_THAT( + app.h264_gst_pipeline, + Equals("videotestsrc pattern=ball is-live=true ! " + "videoscale ! " + "videoconvert ! " + "videorate ! " + "video/x-raw, width={width}, height={height}, framerate={fps}/1,format=I420, chroma-site={color_range}, " + "colorimetry={color_space} ! " + "x264enc pass=qual tune=zerolatency speed-preset=superfast b-adapt=false " + "bframes=0 ref=1 bitrate={bitrate} aud=false sliced-threads=true threads={slices_per_frame} " + "option-string=\"slices={slices_per_frame}:keyint=infinite:open-gop=0\" ! " + "video/x-h264, profile=high, stream-format=byte-stream ! " + "rtpmoonlightpay_video name=moonlight_pay payload_size={payload_size} fec_percentage={fec_percentage} " + "min_required_fec_packets={min_required_fec_packets} ! " + "udpsink host={client_ip} port={client_port}")); + REQUIRE_THAT( + app.hevc_gst_pipeline, + Equals("videotestsrc pattern=ball is-live=true ! " + "videoscale ! " + "videoconvert ! " + "videorate ! " + "video/x-raw, width={width}, height={height}, framerate={fps}/1,format=I420, chroma-site={color_range}, " + "colorimetry={color_space} ! " + "x265enc tune=zerolatency speed-preset=superfast bitrate={bitrate} " + "option-string=\"info=0:keyint=-1:qp=28:repeat-headers=1:slices={slices_per_frame}:frame-threads={" + "slices_per_frame}:aud=0:annexb=1:log-level=3:open-gop=0:bframes=0:intra-refresh=0\" ! " + "video/x-h265, profile=main, stream-format=byte-stream ! " + "rtpmoonlightpay_video name=moonlight_pay payload_size={payload_size} fec_percentage={fec_percentage} " + "min_required_fec_packets={min_required_fec_packets} ! " + "udpsink host={client_ip} port={client_port}")); } SECTION("Paired Clients") {