diff --git a/.devcontainer/dev-setup.sh b/.devcontainer/dev-setup.sh new file mode 100644 index 00000000..93141a55 --- /dev/null +++ b/.devcontainer/dev-setup.sh @@ -0,0 +1,22 @@ +#!/bin/bash +set -e +export DEBIAN_FRONTEND=noninteractive + +apt-get update + +# Install debugger +apt-get install -y gdb + +# Install wayland-protocols +apt-get install -y wayland-protocols + +# Build and install nvtop +apt-get install -y libdrm-dev libsystemd-dev libncurses5-dev libncursesw5-dev +cd /tmp/ +git clone https://github.com/Syllo/nvtop.git +mkdir -p nvtop/build && cd nvtop/build +CXX=/usr/bin/clang++ cmake .. -DNVIDIA_SUPPORT=ON -DAMDGPU_SUPPORT=ON -DINTEL_SUPPORT=ON +cmake --build . --target install --config Release + +# Setup nvidia +bash /etc/cont-init.d/30-nvidia.sh \ No newline at end of file diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 00000000..66193208 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,69 @@ +{ + "name": "wolf-dev", + "build": { + "context": "..", + "dockerfile": "../docker/wolf.Dockerfile", + "target": "wolf-builder" + }, + "postStartCommand": "bash /workspaces/wolf/.devcontainer/dev-setup.sh", + "features": { + "ghcr.io/nils-geistmann/devcontainers-features/zsh:0": { + "plugins": [ + "git", + "zsh-autosuggestions", + "history", + "docker" + ] + }, + "ghcr.io/devcontainers/features/docker-outside-of-docker:1": { + "moby": true, + "installDockerBuildx": false + } + }, + "runArgs": [ + "--network=host", + "-v", + "/tmp/sockets:/tmp/sockets:rw", + "-v", + "/etc/wolf:/etc/wolf:rw", + "-v", + "/run/udev:/run/udev:ro", + "--rm" + ], + "hostRequirements": { + "gpu": true + }, + "privileged": true, + "containerEnv": { + "NVIDIA_DRIVER_CAPABILITIES": "all", + "NVIDIA_VISIBLE_DEVICES": "all" + }, + "remoteEnv": { + "XDG_RUNTIME_DIR": "/tmp/sockets", + "WOLF_LOG_LEVEL": "DEBUG", + "HOST_APPS_STATE_FOLDER": "/etc/wolf", + "WOLF_CFG_FILE": "/etc/wolf/cfg/config.toml", + "WOLF_PRIVATE_KEY_FILE": "/etc/wolf/cfg/key.pem", + "WOLF_PRIVATE_CERT_FILE": "/etc/wolf/cfg/cert.pem", + "WOLF_PULSE_IMAGE": "ghcr.io/games-on-whales/pulseaudio:master", + "WOLF_RENDER_NODE": "/dev/dri/renderD128", + "WOLF_STOP_CONTAINER_ON_EXIT": "TRUE", + "WOLF_DOCKER_SOCKET": "/var/run/docker.sock", + "WOLF_DOCKER_FAKE_UDEV_PATH": "/wolf/fake-udev", + "RUST_BACKTRACE": "full", + "RUST_LOG": "INFO", + "GST_DEBUG": "2", + "CMAKE_GENERATOR": "Ninja" + }, + "customizations": { + "vscode": { + "extensions": [ + "ms-vscode.cpptools-extension-pack", + "ms-azuretools.vscode-docker", + "github.vscode-github-actions", + "jeff-hykin.better-cpp-syntax", + "tamasfe.even-better-toml" + ] + } + } +} \ No newline at end of file diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index 171179b9..4c71de23 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -109,9 +109,9 @@ jobs: file: docker/gstreamer.Dockerfile push: true build-args: | - GSTREAMER_VERSION=1.22.7 + GSTREAMER_VERSION=1.24.6 BASE_IMAGE=ghcr.io/${{ github.repository_owner }}/gpu-drivers:2023.11 - tags: ghcr.io/${{ github.repository_owner }}/gstreamer:1.22.7,gameonwhales/gstreamer:1.22.7 # TODO: set gstreamer version as param + tags: ghcr.io/${{ github.repository_owner }}/gstreamer:1.24.6,gameonwhales/gstreamer:1.24.6 # TODO: set gstreamer version as param labels: ${{ steps.meta.outputs.labels }} cache-from: type=registry,ref=ghcr.io/${{ github.repository_owner }}/gstreamer:buildcache cache-to: type=registry,ref=ghcr.io/${{ github.repository_owner }}/gstreamer:buildcache,mode=max @@ -126,7 +126,7 @@ jobs: tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} build-args: | - BASE_IMAGE=ghcr.io/${{ github.repository_owner }}/gstreamer:1.22.7 + BASE_IMAGE=ghcr.io/${{ github.repository_owner }}/gstreamer:1.24.6 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 + cache-to: ${{ steps.prep.outputs.cache_to }} diff --git a/.gitignore b/.gitignore index cdd93ca9..5e35023f 100644 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,6 @@ DartConfiguration.tcl .run/ target Cargo.lock - +build/ .clj-kondo .lsp diff --git a/docker/gstreamer.Dockerfile b/docker/gstreamer.Dockerfile index 16f56832..25812cfd 100644 --- a/docker/gstreamer.Dockerfile +++ b/docker/gstreamer.Dockerfile @@ -4,8 +4,11 @@ ENV DEBIAN_FRONTEND=noninteractive ENV BUILD_ARCHITECTURE=amd64 ENV DEB_BUILD_OPTIONS=noddebs -ARG GSTREAMER_VERSION=1.22.7 +ARG GSTREAMER_VERSION=1.25.1 ENV GSTREAMER_VERSION=$GSTREAMER_VERSION +# Change this to 1.25.1 once released +ARG GSTREAMER_SHA_COMMIT=671281d860899e9a236f604076831a9ce72186b8 +ENV GSTREAMER_SHA_COMMIT=$GSTREAMER_SHA_COMMIT ENV SOURCE_PATH=/sources/ WORKDIR $SOURCE_PATH @@ -42,8 +45,10 @@ RUN <<_GSTREAMER_INSTALL apt-get install -y --no-install-recommends $DEV_PACKAGES # Build gstreamer - git clone -b $GSTREAMER_VERSION --depth=1 https://gitlab.freedesktop.org/gstreamer/gstreamer.git $SOURCE_PATH/gstreamer + git clone https://gitlab.freedesktop.org/gstreamer/gstreamer.git $SOURCE_PATH/gstreamer cd ${SOURCE_PATH}/gstreamer + git checkout $GSTREAMER_SHA_COMMIT + git submodule update --recursive --remote # see the list of possible options here: https://gitlab.freedesktop.org/gstreamer/gstreamer/-/blob/main/meson_options.txt \ meson setup \ --buildtype=release \ diff --git a/docker/wolf.Dockerfile b/docker/wolf.Dockerfile index f91c02e3..781c25ab 100644 --- a/docker/wolf.Dockerfile +++ b/docker/wolf.Dockerfile @@ -1,4 +1,4 @@ -ARG BASE_IMAGE=ghcr.io/games-on-whales/gstreamer:1.22.7 +ARG BASE_IMAGE=ghcr.io/games-on-whales/gstreamer:1.24.6 ######################################################## FROM $BASE_IMAGE AS wolf-builder diff --git a/docs/modules/dev/pages/manual_build.adoc b/docs/modules/dev/pages/manual_build.adoc index fef23cb8..fd1170b9 100644 --- a/docs/modules/dev/pages/manual_build.adoc +++ b/docs/modules/dev/pages/manual_build.adoc @@ -32,7 +32,7 @@ apt install -y build-essential ninja-build gcc meson cmake ccache bison equivs \ .Build gstreamer [source,bash] .... -git clone -b 1.22.7 --depth=1 https://gitlab.freedesktop.org/gstreamer/gstreamer.git +git clone -b 1.24.6 --depth=1 https://gitlab.freedesktop.org/gstreamer/gstreamer.git cd gstreamer # Setup a place where we'll put the libraries mkdir -p $HOME/gstreamer/include -p $HOME/gstreamer/usr/local/include diff --git a/docs/modules/user/pages/configuration.adoc b/docs/modules/user/pages/configuration.adoc index ab903eee..ac7570e4 100644 --- a/docs/modules/user/pages/configuration.adoc +++ b/docs/modules/user/pages/configuration.adoc @@ -51,6 +51,10 @@ Wolf is configured via a TOML config file and some additional optional ENV varia |/dev/dri/renderD128 |The default render node used for virtual desktops; see: <<_multiple_gpu>> +|WOLF_ENCODER_NODE +|$WOLF_RENDER_NODE +|The default render node used for the Gstreamer pipelines; see: <<_multiple_gpu>> + |WOLF_DOCKER_FAKE_UDEV_PATH |$HOST_APPS_STATE_FOLDER/fake-udev |The path on the host for the fake-udev CLI tool @@ -92,6 +96,12 @@ Can be changed in the `config.toml` file === Share home folder with multiple clients +[WARNING] +==== +This will break isolation, if you want to connect with multiple clients at the same time you should not share the home folder. +You can follow development of that feature https://github.com/games-on-whales/wolf/issues/83[here] +==== + By default, Wolf will create a new home folder for each client, but if you want to share the same home folder with multiple clients, you can set the `app_state_folder` to the same value for each paired client; example: + [source,toml] @@ -259,43 +269,19 @@ You can read more about gstreamer and custom pipelines in the xref:gstreamer.ado When you have multiple GPUs installed in your host, you might want to have better control over which one is used by Wolf and how. + There are two main separated parts that make use of HW acceleration in Wolf: -* Gstreamer video encoding: this will use HW acceleration in order to efficiently encode the video stream with H.264 or HEVC. -* App render node: this will use HW acceleration in order to create virtual Wayland desktops and run the chosen app (ex: Firefox, Steam, ...) +* *App render node*: this will use HW acceleration in order to create virtual Wayland desktops and run the chosen app (ex: Firefox, Steam, ...). +Use the `WOLF_RENDER_NODE` (defaults to `/dev/dri/renderD128`) env variable to control this. +* *Gstreamer video encoding*: this will use HW acceleration in order to efficiently encode the video stream with H.264 or HEVC. +Use the `WOLF_ENCODER_NODE` (defaults to `WOLF_RENDER_NODE`) env variable to control this. -They can be configured separately, and ideally you could even *use two GPUs at the same time* for different jobs; a common setup would be to use the integrated GPU just for the streaming part and use a powerful GPU to play apps/games. - -=== Gstreamer video encoding - -The streaming video encoding pipeline is fully controlled by the `config.toml` file; here the order in which entries are listed is important because Wolf will just try each listed plugin; the first one that works is the one that will be used. +They can be configured separately, and in theory you could even *use two GPUs at the same time* for different jobs; ex: use the integrated GPU just for the streaming part and use a powerful GPU to play apps/games. -[NOTE,caption=EXAMPLE] +[WARNING] ==== -If you have an Intel iGPU and a Nvidia card in the same host, and you would like to use QuickSync in order to do the encoding, you can either: - -* Delete the `nvcodec` entries under `gstreamer.video.hevc_encoders` -* Cut the `qsv` entry and paste it above the `nvcodec` entry +This isn't recommended, it might introduce additional latency and it's not optimal. +HW encoding on modern GPUs is very lightweight and it's better to use the same GPU for both jobs. ==== -On top of that, each single `apps` entry support overriding the default streaming pipeline; for example: - -[source,toml] -.... -[[apps]] -title = "Test ball" - -# More options here, removed for brevity... - -[apps.video] -source = """ -videotestsrc pattern=ball flip=true is-live=true ! -video/x-raw, framerate={fps}/1 -\ -""" -.... - -In case you have two GPUs that will use the same encoder pipeline (example: an AMD iGPU and an AMD GPU card) you can override the `encoder_pipeline` with the corresponding encoder plugin; see: -https://gitlab.freedesktop.org/gstreamer/gstreamer/-/issues/1167[gstreamer/issues/1167]. - === App render node Each application that Wolf will start will have access only to a specific render node even if the host has multiple GPUs connected. + @@ -331,6 +317,40 @@ image = "ghcr.io/games-on-whales/steam:edge" render_node = "/dev/dri/renderD129" .... +=== Gstreamer video encoding + +The easy way to control this is to just edit the env variable `WOLF_ENCODER_NODE` (defaults to match `WOLF_RENDER_NODE` in order to use the same GPU for both), this will set the default render node used for the Gstreamer pipelines. + +The streaming video encoding pipeline is fully controlled by the `config.toml` file; here the order in which entries are listed is important because Wolf will just try each listed plugin; the first one that works is the one that will be used. + +[NOTE,caption=EXAMPLE] +==== +If you have an Intel iGPU and a Nvidia card in the same host, and you would like to use QuickSync in order to do the encoding, you can either: + +* Delete the `nvcodec` entries under `gstreamer.video.hevc_encoders` +* Cut the `qsv` entry and paste it above the `nvcodec` entry +==== + +On top of that, each single `apps` entry support overriding the default streaming pipeline; for example: + +[source,toml] +.... +[[apps]] +title = "Test ball" + +# More options here, removed for brevity... + +[apps.video] +source = """ +videotestsrc pattern=ball flip=true is-live=true ! +video/x-raw, framerate={fps}/1 +\ +""" +.... + +In case you have two GPUs that will use the same encoder pipeline (example: an AMD iGPU and an AMD GPU card) you can override the `video_params` with the corresponding encoder plugin; see: +https://gitlab.freedesktop.org/gstreamer/gstreamer/-/issues/1167[gstreamer/issues/1167]. + == Directly launch a Steam game In order to directly launch a Steam game from Moonlight you can just copy the existing `[[apps]]` entry for Steam, change the name and just add the Steam app ID as env variable; example: diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 2a582866..6691ca4f 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -56,7 +56,7 @@ if (UNIX AND NOT APPLE) FetchContent_Declare( inputtino GIT_REPOSITORY https://github.com/games-on-whales/inputtino.git - GIT_TAG 56bc064) + GIT_TAG 5d4b8b2) FetchContent_MakeAvailable(inputtino) endif () diff --git a/src/core/src/core/audio.hpp b/src/core/src/core/audio.hpp index d15687b2..db1dbf39 100644 --- a/src/core/src/core/audio.hpp +++ b/src/core/src/core/audio.hpp @@ -8,6 +8,7 @@ #include #include #include +#include namespace wolf::core::audio { @@ -15,10 +16,33 @@ typedef struct Server Server; std::shared_ptr connect(std::string_view server = {}); +constexpr auto SAMPLE_RATE = 48000; + +struct AudioMode { + + enum Speakers { + FRONT_LEFT, + FRONT_RIGHT, + FRONT_CENTER, + LOW_FREQUENCY, + BACK_LEFT, + BACK_RIGHT, + SIDE_LEFT, + SIDE_RIGHT, + MAX_SPEAKERS, + }; + + int channels{}; + int streams{}; + int coupled_streams{}; + std::vector speakers; + int bitrate{}; + int sample_rate = SAMPLE_RATE; +}; + struct AudioDevice { std::string_view sink_name; - int n_channels; - int bitrate = 48000; + AudioMode mode; }; struct VSink { diff --git a/src/core/src/platforms/linux/pulseaudio/pulse.cpp b/src/core/src/platforms/linux/pulseaudio/pulse.cpp index 376c4ade..cfff4dce 100644 --- a/src/core/src/platforms/linux/pulseaudio/pulse.cpp +++ b/src/core/src/platforms/linux/pulseaudio/pulse.cpp @@ -3,6 +3,51 @@ #include #include +namespace fmt { +template <> class formatter { +public: + template constexpr auto parse(ParseContext &ctx) { + return ctx.begin(); + } + + template + auto format(const wolf::core::audio::AudioMode::Speakers &speaker, FormatContext &ctx) const { + std::string speaker_name; + // Mapping taken from + // https://www.freedesktop.org/wiki/Software/PulseAudio/Documentation/User/Modules/#module-null-sink + switch (speaker) { + case wolf::core::audio::AudioMode::FRONT_LEFT: + speaker_name = "front-left"; + break; + case wolf::core::audio::AudioMode::FRONT_RIGHT: + speaker_name = "front-right"; + break; + case wolf::core::audio::AudioMode::FRONT_CENTER: + speaker_name = "front-center"; + break; + case wolf::core::audio::AudioMode::LOW_FREQUENCY: + speaker_name = "lfe"; + break; + case wolf::core::audio::AudioMode::BACK_LEFT: + speaker_name = "rear-left"; + break; + case wolf::core::audio::AudioMode::BACK_RIGHT: + speaker_name = "rear-right"; + break; + case wolf::core::audio::AudioMode::SIDE_LEFT: + speaker_name = "side-left"; + break; + case wolf::core::audio::AudioMode::SIDE_RIGHT: + speaker_name = "side-right"; + break; + case wolf::core::audio::AudioMode::MAX_SPEAKERS: + break; + } + return fmt::format_to(ctx.out(), "{}", speaker_name); + } +}; +} // namespace fmt + namespace wolf::core::audio { struct Server { @@ -88,8 +133,11 @@ std::shared_ptr create_virtual_sink(const std::shared_ptr &server queue_op(server, [server, vsink]() { auto device = vsink->device; - auto channel_spec = - fmt::format("rate={} sink_name={} channels={}", device.bitrate, device.sink_name, device.n_channels); + auto channel_spec = fmt::format("rate={} sink_name={} channels={} channel_map={}", + device.mode.sample_rate, + device.sink_name, + device.mode.channels, + fmt::join(device.mode.speakers, ",")); auto operation = pa_context_load_module( server->ctx, "module-null-sink", diff --git a/src/moonlight-protocol/CMakeLists.txt b/src/moonlight-protocol/CMakeLists.txt index 5e3ebc32..57f08cb3 100644 --- a/src/moonlight-protocol/CMakeLists.txt +++ b/src/moonlight-protocol/CMakeLists.txt @@ -45,18 +45,20 @@ FetchContent_GetProperties(nanors) if (NOT nanors_POPULATED) FetchContent_Populate(nanors) - add_library(nanors STATIC ${nanors_SOURCE_DIR}/rs.c) + add_library(nanors) add_library(nanors::nanors ALIAS nanors) - target_include_directories(nanors PUBLIC ${nanors_SOURCE_DIR} ${nanors_SOURCE_DIR}/deps/obl/) + target_include_directories(nanors + PUBLIC + ./nanors + ${nanors_SOURCE_DIR} + ${nanors_SOURCE_DIR}/deps/obl/) target_sources(nanors - PRIVATE ${nanors_SOURCE_DIR}/rs.c - PUBLIC ${nanors_SOURCE_DIR}/rs.h) + PRIVATE ./nanors/rswrapper.c + PUBLIC ./nanors/rswrapper.h) - # TODO: There's a more advanced version of this with proper support for SSSE3, - # see: https://github.com/LizardByte/Sunshine/pull/2828 - set_source_files_properties(${nanors_SOURCE_DIR}/rs.c - PROPERTIES COMPILE_FLAGS "-include deps/obl/autoshim.h -ftree-vectorize") + set_source_files_properties(./nanors/rswrapper.c + PROPERTIES COMPILE_FLAGS "-ftree-vectorize -funroll-loops") target_link_libraries_system(moonlight PUBLIC nanors::nanors) endif () diff --git a/src/moonlight-protocol/moonlight.cpp b/src/moonlight-protocol/moonlight.cpp index 1f667400..ab2f6a59 100644 --- a/src/moonlight-protocol/moonlight.cpp +++ b/src/moonlight-protocol/moonlight.cpp @@ -7,9 +7,9 @@ namespace moonlight { constexpr int VIDEO_FORMAT_H264 = 0x0001; constexpr int VIDEO_FORMAT_H265 = 0x0100; -constexpr int VIDEO_FORMAT_H265_MAIN10 = 0x0200; -constexpr int VIDEO_FORMAT_AV1_MAIN8 = 0x1000; -constexpr int VIDEO_FORMAT_AV1_MAIN10 = 0x2000; +constexpr int VIDEO_FORMAT_H265_MAIN10 = 0x00200; +constexpr int VIDEO_FORMAT_AV1_MAIN8 = 0x10000; +constexpr int VIDEO_FORMAT_AV1_MAIN10 = 0x20000; XML serverinfo(bool isServerBusy, int current_appid, diff --git a/src/moonlight-protocol/moonlight/fec.hpp b/src/moonlight-protocol/moonlight/fec.hpp index ec15a67c..66338963 100644 --- a/src/moonlight-protocol/moonlight/fec.hpp +++ b/src/moonlight-protocol/moonlight/fec.hpp @@ -3,7 +3,7 @@ #include extern "C" { -#include +#include "rswrapper.h" } /** @@ -32,7 +32,7 @@ inline void init() { /** * A smart pointer to the reed_solomon data structure, it will release the memory when going out of scope */ -using rs_ptr = std::unique_ptr; +using rs_ptr = std::shared_ptr; /** * Creates and allocates the required Reed Solomon data structure. @@ -43,8 +43,8 @@ using rs_ptr = std::unique_ptr; * @return A smart pointer, it will release the memory when going out of scope */ inline rs_ptr create(int data_shards, int parity_shards) { - auto rs = reed_solomon_new(data_shards, parity_shards); - return {rs, ::reed_solomon_release}; + auto rs = reed_solomon_new_fn(data_shards, parity_shards); + return std::shared_ptr(rs, reed_solomon_release_fn); } /** @@ -63,7 +63,7 @@ inline rs_ptr create(int data_shards, int parity_shards) { * @return zero on success or an error code if failing. */ inline int encode(reed_solomon *rs, uint8_t **shards, int nr_shards, int block_size) { - return reed_solomon_encode(rs, shards, nr_shards, block_size); + return reed_solomon_encode_fn(rs, shards, nr_shards, block_size); } /** @@ -82,7 +82,7 @@ inline int encode(reed_solomon *rs, uint8_t **shards, int nr_shards, int block_s * @return zero on success or an error code if failing */ inline int decode(reed_solomon *rs, uint8_t **shards, uint8_t *marks, int nr_shards, int block_size) { - return reed_solomon_decode(rs, shards, marks, nr_shards, block_size); + return reed_solomon_decode_fn(rs, shards, marks, nr_shards, block_size); } } // namespace moonlight::fec \ No newline at end of file diff --git a/src/moonlight-protocol/nanors/rswrapper.c b/src/moonlight-protocol/nanors/rswrapper.c new file mode 100644 index 00000000..dd3c08e5 --- /dev/null +++ b/src/moonlight-protocol/nanors/rswrapper.c @@ -0,0 +1,153 @@ +/** + * @file src/rswrapper.c + * @brief Wrappers for nanors vectorization with different ISA options + */ + +// _FORTIY_SOURCE can cause some versions of GCC to try to inline +// memset() with incompatible target options when compiling rs.c +#ifdef _FORTIFY_SOURCE +#undef _FORTIFY_SOURCE +#endif + +// The assert() function is decorated with __cold on macOS which +// is incompatible with Clang's target multiversioning feature +#ifndef NDEBUG +#define NDEBUG +#endif + +#define DECORATE_FUNC_I(a, b) a##b +#define DECORATE_FUNC(a, b) DECORATE_FUNC_I(a, b) + +// Append an ISA suffix to the public RS API +#define reed_solomon_init DECORATE_FUNC(reed_solomon_init, ISA_SUFFIX) +#define reed_solomon_new DECORATE_FUNC(reed_solomon_new, ISA_SUFFIX) +#define reed_solomon_new_static DECORATE_FUNC(reed_solomon_new_static, ISA_SUFFIX) +#define reed_solomon_release DECORATE_FUNC(reed_solomon_release, ISA_SUFFIX) +#define reed_solomon_decode DECORATE_FUNC(reed_solomon_decode, ISA_SUFFIX) +#define reed_solomon_encode DECORATE_FUNC(reed_solomon_encode, ISA_SUFFIX) + +// Append an ISA suffix to internal functions to prevent multiple definition errors +#define obl_axpy_ref DECORATE_FUNC(obl_axpy_ref, ISA_SUFFIX) +#define obl_scal_ref DECORATE_FUNC(obl_scal_ref, ISA_SUFFIX) +#define obl_axpyb32_ref DECORATE_FUNC(obl_axpyb32_ref, ISA_SUFFIX) +#define obl_axpy DECORATE_FUNC(obl_axpy, ISA_SUFFIX) +#define obl_scal DECORATE_FUNC(obl_scal, ISA_SUFFIX) +#define obl_swap DECORATE_FUNC(obl_swap, ISA_SUFFIX) +#define obl_axpyb32 DECORATE_FUNC(obl_axpyb32, ISA_SUFFIX) +#define axpy DECORATE_FUNC(axpy, ISA_SUFFIX) +#define scal DECORATE_FUNC(scal, ISA_SUFFIX) +#define gemm DECORATE_FUNC(gemm, ISA_SUFFIX) +#define invert_mat DECORATE_FUNC(invert_mat, ISA_SUFFIX) + +#if defined(__x86_64__) || defined(__i386__) + +// Compile a variant for SSSE3 +#if defined(__clang__) +#pragma clang attribute push(__attribute__((target("ssse3"))), apply_to = function) +#else +#pragma GCC push_options +#pragma GCC target("ssse3") +#endif +#define ISA_SUFFIX _ssse3 +#define OBLAS_SSE3 +#include "./rs.c" +#undef OBLAS_SSE3 +#undef ISA_SUFFIX +#if defined(__clang__) +#pragma clang attribute pop +#else +#pragma GCC pop_options +#endif + +// Compile a variant for AVX2 +#if defined(__clang__) +#pragma clang attribute push(__attribute__((target("avx2"))), apply_to = function) +#else +#pragma GCC push_options +#pragma GCC target("avx2") +#endif +#define ISA_SUFFIX _avx2 +#define OBLAS_AVX2 +#include "./rs.c" +#undef OBLAS_AVX2 +#undef ISA_SUFFIX +#if defined(__clang__) +#pragma clang attribute pop +#else +#pragma GCC pop_options +#endif + +// Compile a variant for AVX512BW +#if defined(__clang__) +#pragma clang attribute push(__attribute__((target("avx512f,avx512bw"))), apply_to = function) +#else +#pragma GCC push_options +#pragma GCC target("avx512f,avx512bw") +#endif +#define ISA_SUFFIX _avx512 +#define OBLAS_AVX512 +#include "./rs.c" +#undef OBLAS_AVX512 +#undef ISA_SUFFIX +#if defined(__clang__) +#pragma clang attribute pop +#else +#pragma GCC pop_options +#endif + +#endif + +// Compile a default variant +#define ISA_SUFFIX _def +#include "./autoshim.h" +#include "./rs.c" +#undef ISA_SUFFIX + +#undef reed_solomon_init +#undef reed_solomon_new +#undef reed_solomon_new_static +#undef reed_solomon_release +#undef reed_solomon_decode +#undef reed_solomon_encode + +#include "rswrapper.h" + +reed_solomon_new_t reed_solomon_new_fn; +reed_solomon_release_t reed_solomon_release_fn; +reed_solomon_encode_t reed_solomon_encode_fn; +reed_solomon_decode_t reed_solomon_decode_fn; + +/** + * @brief This initializes the RS function pointers to the best vectorized version available. + * @details The streaming code will directly invoke these function pointers during encoding. + */ +void reed_solomon_init(void) { +#if defined(__x86_64__) || defined(__i386__) + if (__builtin_cpu_supports("avx512f") && __builtin_cpu_supports("avx512bw")) { + reed_solomon_new_fn = reed_solomon_new_avx512; + reed_solomon_release_fn = reed_solomon_release_avx512; + reed_solomon_encode_fn = reed_solomon_encode_avx512; + reed_solomon_decode_fn = reed_solomon_decode_avx512; + reed_solomon_init_avx512(); + } else if (__builtin_cpu_supports("avx2")) { + reed_solomon_new_fn = reed_solomon_new_avx2; + reed_solomon_release_fn = reed_solomon_release_avx2; + reed_solomon_encode_fn = reed_solomon_encode_avx2; + reed_solomon_decode_fn = reed_solomon_decode_avx2; + reed_solomon_init_avx2(); + } else if (__builtin_cpu_supports("ssse3")) { + reed_solomon_new_fn = reed_solomon_new_ssse3; + reed_solomon_release_fn = reed_solomon_release_ssse3; + reed_solomon_encode_fn = reed_solomon_encode_ssse3; + reed_solomon_decode_fn = reed_solomon_decode_ssse3; + reed_solomon_init_ssse3(); + } else +#endif + { + reed_solomon_new_fn = reed_solomon_new_def; + reed_solomon_release_fn = reed_solomon_release_def; + reed_solomon_encode_fn = reed_solomon_encode_def; + reed_solomon_decode_fn = reed_solomon_decode_def; + reed_solomon_init_def(); + } +} diff --git a/src/moonlight-protocol/nanors/rswrapper.h b/src/moonlight-protocol/nanors/rswrapper.h new file mode 100644 index 00000000..f74f70bc --- /dev/null +++ b/src/moonlight-protocol/nanors/rswrapper.h @@ -0,0 +1,21 @@ +/** + * @file src/rswrapper.h + * @brief Wrappers for nanors vectorization + * @details This is a drop-in replacement for nanors rs.h + */ +#pragma once + +#include "rs.h" +#include + +typedef struct _reed_solomon reed_solomon; + +typedef reed_solomon *(*reed_solomon_new_t)(int data_shards, int parity_shards); +typedef void (*reed_solomon_release_t)(reed_solomon *rs); +typedef int (*reed_solomon_encode_t)(reed_solomon *rs, uint8_t **shards, int nr_shards, int bs); +typedef int (*reed_solomon_decode_t)(reed_solomon *rs, uint8_t **shards, uint8_t *marks, int nr_shards, int bs); + +extern reed_solomon_new_t reed_solomon_new_fn; +extern reed_solomon_release_t reed_solomon_release_fn; +extern reed_solomon_encode_t reed_solomon_encode_fn; +extern reed_solomon_decode_t reed_solomon_decode_fn; \ No newline at end of file diff --git a/src/moonlight-server/CMakeLists.txt b/src/moonlight-server/CMakeLists.txt index 7e7e6409..beac5c57 100644 --- a/src/moonlight-server/CMakeLists.txt +++ b/src/moonlight-server/CMakeLists.txt @@ -34,7 +34,7 @@ target_link_libraries_system(wolf_runner PUBLIC simple-web-server) FetchContent_Declare( toml GIT_REPOSITORY https://github.com/ToruNiina/toml11.git - GIT_TAG v3.7.1) + GIT_TAG v4.2.0) set(TOML11_COLORIZE_ERROR_MESSAGE, ON) set(TOML11_PRESERVE_COMMENTS_BY_DEFAULT, ON) FetchContent_MakeAvailable(toml) @@ -139,7 +139,7 @@ function(make_includable input_file output_file) endfunction(make_includable) make_includable(rest/html/pin.html rest/html/pin.include.html) -make_includable(state/default/config.v2.toml state/default/config.include.toml) +make_includable(state/default/config.v4.toml state/default/config.include.toml) # All users of this library will need at least C++17 target_compile_features(wolf_runner PUBLIC cxx_std_17) diff --git a/src/moonlight-server/gst-plugin/video.hpp b/src/moonlight-server/gst-plugin/video.hpp index a706a7e0..cc4904b6 100644 --- a/src/moonlight-server/gst-plugin/video.hpp +++ b/src/moonlight-server/gst-plugin/video.hpp @@ -199,7 +199,7 @@ static void generate_fec_packets(const gst_rtp_moonlight_pay_video &rtpmoonlight auto payload_size = (int)gst_buffer_get_size(rtp_payload); auto blocks = determine_split(rtpmoonlightpay, gst_buffer_list_length(rtp_packets)); - auto nr_shards = blocks.data_shards + blocks.parity_shards; + const auto nr_shards = blocks.data_shards + blocks.parity_shards; if (nr_shards > DATA_SHARDS_MAX) { logs::log(logs::warning, @@ -223,11 +223,11 @@ static void generate_fec_packets(const gst_rtp_moonlight_pay_video &rtpmoonlight // Reed Solomon encode the full stream of bytes auto rs = moonlight::fec::create(blocks.data_shards, blocks.parity_shards); - unsigned char *ptr[nr_shards]; + std::vector ptr(nr_shards); for (int shard_idx = 0; shard_idx < nr_shards; shard_idx++) { ptr[shard_idx] = info.data + (shard_idx * blocks.block_size); } - if (moonlight::fec::encode(rs.get(), ptr, nr_shards, blocks.block_size) != 0) { + if (moonlight::fec::encode(rs.get(), &ptr.front(), nr_shards, blocks.block_size) != 0) { logs::log(logs::warning, "Error during video FEC encoding"); } diff --git a/src/moonlight-server/rest/endpoints.hpp b/src/moonlight-server/rest/endpoints.hpp index 3ee0c9bf..2d85cd40 100644 --- a/src/moonlight-server/rest/endpoints.hpp +++ b/src/moonlight-server/rest/endpoints.hpp @@ -238,18 +238,18 @@ void applist(const std::shared_ptr: state::StreamSession create_run_session(const std::shared_ptr::Request> &request, const state::PairedClient ¤t_client, - std::shared_ptr event_bus, + immer::box state, const state::App &run_app) { SimpleWeb::CaseInsensitiveMultimap headers = request->parse_query_string(); auto display_mode_str = utils::split(get_header(headers, "mode").value_or("1920x1080x60"), 'x'); moonlight::DisplayMode display_mode = {std::stoi(display_mode_str[0].data()), std::stoi(display_mode_str[1].data()), - std::stoi(display_mode_str[2].data())}; + std::stoi(display_mode_str[2].data()), + state->config->support_hevc, + state->config->support_av1}; - // forcing stereo, TODO: what should we select here? - state::AudioMode audio_mode = {2, 1, 1, {state::AudioMode::FRONT_LEFT, state::AudioMode::FRONT_RIGHT}}; - - // auto joypad_map = get_header(headers, "remoteControllersBitmap").value(); // TODO: decipher this (might be empty) + auto surround_info = std::stoi(get_header(headers, "surroundAudioInfo").value_or("196610")); + int channelCount = surround_info & (65535); std::string host_state_folder = utils::get_env("HOST_APPS_STATE_FOLDER", "/etc/wolf"); auto full_path = std::filesystem::path(host_state_folder) / current_client.app_state_folder / run_app.base.title; @@ -257,8 +257,8 @@ create_run_session(const std::shared_ptrevent_bus, .app = std::make_shared(run_app), .app_state_folder = full_path.string(), @@ -300,7 +300,7 @@ void launch(const std::shared_ptr:: SimpleWeb::CaseInsensitiveMultimap headers = request->parse_query_string(); auto app = state::get_app_by_id(state->config, get_header(headers, "appid").value()); - auto new_session = create_run_session(request, current_client, state->event_bus, app); + auto new_session = create_run_session(request, current_client, state, app); state->event_bus->fire_event(immer::box(new_session)); state->running_sessions->update( [&new_session](const immer::vector &ses_v) { return ses_v.push_back(new_session); }); @@ -321,7 +321,7 @@ void resume(const std::shared_ptr:: auto client_ip = get_client_ip(request); auto old_session = get_session_by_ip(state->running_sessions->load(), client_ip); if (old_session) { - auto new_session = create_run_session(request, current_client, state->event_bus, *old_session->app); + auto new_session = create_run_session(request, current_client, state, *old_session->app); // Carry over the old session display handle new_session.wayland_display = std::move(old_session->wayland_display); // Carry over the old session devices, they'll be already plugged into the container diff --git a/src/moonlight-server/rtsp/commands.hpp b/src/moonlight-server/rtsp/commands.hpp index 7f51b6b8..f99200d1 100644 --- a/src/moonlight-server/rtsp/commands.hpp +++ b/src/moonlight-server/rtsp/commands.hpp @@ -37,6 +37,7 @@ RTSP_PACKET ok_msg(int sequence_number, // Additional feature supports constexpr uint32_t FS_PEN_TOUCH_EVENTS = 0x01; constexpr uint32_t FS_CONTROLLER_TOUCH_EVENTS = 0x02; +using namespace wolf::core::audio; RTSP_PACKET describe(const RTSP_PACKET &req, const state::StreamSession &session) { @@ -48,16 +49,55 @@ describe(const RTSP_PACKET &req, const state::StreamSession &session) { payloads.push_back({"a", "a=rtpmap:98 AV1/90000"}); } - std::string audio_speakers = session.audio_mode.speakers // - | views::transform([](auto speaker) { return (char)(speaker + '0'); }) // - | to; // + // Advertise all audio configurations + for (const auto audio_mode : state::AUDIO_CONFIGURATIONS) { + auto mapping_p = audio_mode.speakers; + // Opusenc forces a re-mapping to Vorbis; see + // https://gitlab.freedesktop.org/gstreamer/gstreamer/-/blob/1.24.6/subprojects/gst-plugins-base/ext/opus/gstopusenc.c#L549-572 + if (audio_mode.channels == 6) { // 5.1 + mapping_p = { + // The mapping for 5.1 is: [0 1 4 5 2 3] + AudioMode::FRONT_LEFT, + AudioMode::FRONT_RIGHT, + AudioMode::BACK_LEFT, + AudioMode::BACK_RIGHT, + AudioMode::FRONT_CENTER, + AudioMode::LOW_FREQUENCY, + }; + } else if (audio_mode.channels == 8) { // 7.1 + mapping_p = { + // The mapping for 7.1 is: [0 1 4 5 2 3 6 7] + AudioMode::FRONT_LEFT, + AudioMode::FRONT_RIGHT, + AudioMode::BACK_LEFT, + AudioMode::BACK_RIGHT, + AudioMode::FRONT_CENTER, + AudioMode::LOW_FREQUENCY, + AudioMode::SIDE_LEFT, + AudioMode::SIDE_RIGHT, + }; + } - payloads.push_back({"a", - fmt::format("fmtp:97 surround-params={}{}{}{}", - session.audio_mode.channels, - session.audio_mode.streams, - session.audio_mode.coupled_streams, - audio_speakers)}); + /** + * GFE advertises incorrect mapping for normal quality configurations, + * as a result, Moonlight rotates all channels from index '3' to the right + * To work around this, rotate channels to the left from index '3' + */ + if (audio_mode.channels > 2) { // 5.1 and 7.1 + std::rotate(mapping_p.begin() + 3, mapping_p.begin() + 4, mapping_p.end()); + } + std::string audio_speakers = mapping_p // + | views::transform([](auto speaker) { return (char)(speaker + '0'); }) // + | to; + auto surround_params = fmt::format("fmtp:97 surround-params={}{}{}{}", + audio_mode.channels, + audio_mode.streams, + audio_mode.coupled_streams, + audio_speakers); + + payloads.push_back({"a", surround_params}); + logs::log(logs::trace, "[RTSP] Sending audio surround params: {}", surround_params); + } payloads.push_back( {"a", fmt::format("x-ss-general.featureFlags: {}", FS_PEN_TOUCH_EVENTS | FS_CONTROLLER_TOUCH_EVENTS)}); @@ -146,6 +186,31 @@ announce(const RTSP_PACKET &req, gst_pipeline = session.app->h264_gst_pipeline; } + auto audio_channels = args["x-nv-audio.surround.numChannels"].value_or(session.audio_channel_count); + auto fec_percentage = 20; // TODO: setting? + + long bitrate = args["x-nv-vqos[0].bw.maximumBitrateKbps"].value_or(15500); + // If the client sent a configured bitrate adjust it (Moonlight extension) + if (auto configured_bitrate = args["x-ml-video.configuredBitrateKbps"]; configured_bitrate.has_value()) { + bitrate = *configured_bitrate; + + // If the FEC percentage isn't too high, adjust the configured bitrate to ensure video + // traffic doesn't exceed the user's selected bitrate when the FEC shards are included. + if (fec_percentage <= 80) { + bitrate /= 100.f / (100 - fec_percentage); + } + + // Adjust the bitrate to account for audio traffic bandwidth usage (capped at 20% reduction). + // The bitrate per channel is 256 Kbps for high quality mode and 96 Kbps for normal quality. + auto audioBitrateAdjustment = 96 * audio_channels; + bitrate -= std::min((std::int64_t)audioBitrateAdjustment, bitrate / 5); + + // Reduce it by another 500Kbps to account for A/V packet overhead and control data + // traffic (capped at 10% reduction). + bitrate -= std::min((std::int64_t)500, bitrate / 10); + logs::log(logs::debug, "[RTSP] Adjusted video bitrate to {} Kbps", bitrate); + } + // Video session unsigned short video_port = state::VIDEO_PING_PORT + number_of_sessions; state::VideoSession video = { @@ -158,9 +223,9 @@ announce(const RTSP_PACKET &req, .timeout = std::chrono::milliseconds(args["x-nv-video[0].timeoutLengthMs"].value_or(7000)), .packet_size = args["x-nv-video[0].packetSize"].value_or(1024), .frames_with_invalid_ref_threshold = args["x-nv-video[0].framesWithInvalidRefThreshold"].value_or(0), - .fec_percentage = 20, + .fec_percentage = fec_percentage, .min_required_fec_packets = args["x-nv-vqos[0].fec.minRequiredFecPackets"].value_or(0), - .bitrate_kbps = args["x-nv-video[0].initialBitrateKbps"].value_or(15500), + .bitrate_kbps = bitrate, .slices_per_frame = args["x-nv-video[0].videoEncoderSlicesPerFrame"].value_or(1), .color_range = (csc & 0x1) ? state::JPEG : state::MPEG, @@ -171,6 +236,8 @@ announce(const RTSP_PACKET &req, // Audio session unsigned short audio_port = state::AUDIO_PING_PORT + number_of_sessions; + auto high_quality_audio = args["x-nv-audio.surround.AudioQuality"].value_or(0) == 1; + auto audio_mode = state::get_audio_mode(audio_channels, high_quality_audio); state::AudioSession audio = { .gst_pipeline = session.app->opus_gst_pipeline, @@ -184,7 +251,7 @@ announce(const RTSP_PACKET &req, .client_ip = session.ip, .packet_duration = args["x-nv-aqos.packetDuration"].value_or(5), - .channels = args["x-nv-audio.surround.numChannels"].value_or(2)}; + .audio_mode = audio_mode}; event_bus->fire_event(immer::box(audio)); return ok_msg(req.seq_number); diff --git a/src/moonlight-server/runners/docker.hpp b/src/moonlight-server/runners/docker.hpp index 6fa32272..19d80ad3 100644 --- a/src/moonlight-server/runners/docker.hpp +++ b/src/moonlight-server/runners/docker.hpp @@ -98,17 +98,18 @@ class RunDocker : public state::Runner { std::string_view render_node) override; toml::value serialise() override { - return {{"type", "docker"}, - {"name", container.name}, - {"image", container.image}, - {"ports", - container.ports | transform([](const auto &el) { return fmt::format("{}", el); }) | ranges::to_vector}, - {"mounts", - container.mounts | transform([](const auto &el) { return fmt::format("{}", el); }) | ranges::to_vector}, - {"devices", - container.devices | transform([](const auto &el) { return fmt::format("{}", el); }) | ranges::to_vector}, - {"env", container.env}, - {"base_create_json", base_create_json}}; + return toml::table{ + {"type", "docker"}, + {"name", container.name}, + {"image", container.image}, + {"ports", + container.ports | transform([](const auto &el) { return fmt::format("{}", el); }) | ranges::to_vector}, + {"mounts", + container.mounts | transform([](const auto &el) { return fmt::format("{}", el); }) | ranges::to_vector}, + {"devices", + container.devices | transform([](const auto &el) { return fmt::format("{}", el); }) | ranges::to_vector}, + {"env", container.env}, + {"base_create_json", base_create_json}}; } protected: diff --git a/src/moonlight-server/runners/process.hpp b/src/moonlight-server/runners/process.hpp index 55e1b60d..6af41366 100644 --- a/src/moonlight-server/runners/process.hpp +++ b/src/moonlight-server/runners/process.hpp @@ -27,7 +27,7 @@ class RunProcess : public state::Runner { std::string_view render_node) override; toml::value serialise() override { - return {{"type", "process"}, {"run_cmd", this->run_cmd}}; + return toml::table{{"type", "process"}, {"run_cmd", this->run_cmd}}; } protected: diff --git a/src/moonlight-server/state/configTOML.cpp b/src/moonlight-server/state/configTOML.cpp index dca0a625..8eef1a16 100644 --- a/src/moonlight-server/state/configTOML.cpp +++ b/src/moonlight-server/state/configTOML.cpp @@ -1,6 +1,7 @@ #include #include #include +#include #include #include #include @@ -10,16 +11,21 @@ namespace state { +struct GstEncoderDefault { + std::string video_params; +}; + struct GstEncoder { std::string plugin_name; std::vector check_elements; - std::string video_params; + std::optional video_params; std::string encoder_pipeline; }; struct GstVideoCfg { std::string default_source; std::string default_sink; + std::map defaults; std::vector av1_encoders; std::vector hevc_encoders; @@ -34,9 +40,10 @@ struct GstAudioCfg { }; } // namespace state +TOML11_DEFINE_CONVERSION_NON_INTRUSIVE(state::GstEncoderDefault, video_params) TOML11_DEFINE_CONVERSION_NON_INTRUSIVE(state::GstEncoder, plugin_name, check_elements, video_params, encoder_pipeline) TOML11_DEFINE_CONVERSION_NON_INTRUSIVE( - state::GstVideoCfg, default_source, default_sink, av1_encoders, hevc_encoders, h264_encoders) + state::GstVideoCfg, default_source, default_sink, defaults, av1_encoders, hevc_encoders, h264_encoders) TOML11_DEFINE_CONVERSION_NON_INTRUSIVE( state::GstAudioCfg, default_source, default_audio_params, default_opus_encoder, default_sink) @@ -45,13 +52,13 @@ namespace toml { template <> struct into { static toml::value into_toml(const state::App &f) { - return toml::value{{"title", f.base.title}, {"support_hdr", f.base.support_hdr}, {"runner", f.runner->serialise()}}; + return toml::table{{"title", f.base.title}, {"support_hdr", f.base.support_hdr}, {"runner", f.runner->serialise()}}; } }; template <> struct into { - static toml::value into_toml(const state::PairedClient &c) { - return toml::value{{"client_cert", c.client_cert}, + template static toml::basic_value into_toml(const state::PairedClient &c) { + return toml::table{{"client_cert", c.client_cert}, {"app_state_folder", c.app_state_folder}, {"run_uid", c.run_uid}, {"run_gid", c.run_gid}}; @@ -59,8 +66,7 @@ template <> struct into { }; template <> struct from { - template class M, template class A> - static state::PairedClient from_toml(const basic_value &v) { + static state::PairedClient from_toml(const toml::value &v) { state::PairedClient client; client.client_cert = find(v, "client_cert"); @@ -88,7 +94,7 @@ using namespace std::literals; void write(const toml::value &data, const std::string &dest) { std::ofstream out_file; out_file.open(dest); - out_file << toml::format(data, 120); + out_file << toml::format(data); out_file.close(); } @@ -102,7 +108,7 @@ void create_default(const std::string &source) { } std::shared_ptr get_runner(const toml::value &item, const std::shared_ptr &ev_bus) { - auto runner_obj = toml::find_or(item, "runner", {{"type", "process"}}); + auto runner_obj = toml::find_or(item, "runner", toml::value{toml::table{{"type", "process"}}}); auto runner_type = toml::find_or(runner_obj, "type", "process"); if (runner_type == "process") { auto run_cmd = toml::find_or(runner_obj, "run_cmd", "sh -c \"while :; do echo 'running...'; sleep 1; done\""); @@ -115,23 +121,8 @@ std::shared_ptr get_runner(const toml::value &item, const std::sh } } -static bool is_available(const GstEncoder &settings) { - if (auto plugin = gst_registry_find_plugin(gst_registry_get(), settings.plugin_name.c_str())) { - gst_object_unref(plugin); - return std::all_of(settings.check_elements.begin(), settings.check_elements.end(), [](const auto &el_name) { - if (auto el = gst_element_factory_make(el_name.c_str(), nullptr)) { - gst_object_unref(el); - return true; - } else { - return false; - } - }); - } - return false; -} - -static state::Encoder encoder_type(const std::string &gstreamer_plugin_name) { - switch (utils::hash(gstreamer_plugin_name)) { +static state::Encoder encoder_type(const GstEncoder &settings) { + switch (utils::hash(settings.plugin_name)) { case (utils::hash("nvcodec")): return NVIDIA; case (utils::hash("vaapi")): @@ -146,40 +137,68 @@ static state::Encoder encoder_type(const std::string &gstreamer_plugin_name) { case (utils::hash("aom")): return SOFTWARE; } - logs::log(logs::warning, "Unrecognised Gstreamer plugin name: {}", gstreamer_plugin_name); + logs::log(logs::warning, "Unrecognised Gstreamer plugin name: {}", settings.plugin_name); return UNKNOWN; } -toml::value v1_to_v2(const toml::value &v1, const std::string &source) { - create_default(source); - auto v2 = toml::parse(source); - v2["hostname"] = v1.at("hostname").as_string(); - v2["uuid"] = v1.at("uuid").as_string(); - v2["support_hevc"] = v1.at("support_hevc").as_boolean(); - v2["paired_clients"] = v1.at("paired_clients").as_array() | // - ranges::views::transform([](const toml::value &client) { // - return PairedClient{.client_cert = client.at("client_cert").as_string()}; // - }) // - | ranges::to(); - write(v2, source); - return v2; +static bool is_available(const GPU_VENDOR &gpu_vendor, const GstEncoder &settings) { + if (auto plugin = gst_registry_find_plugin(gst_registry_get(), settings.plugin_name.c_str())) { + gst_object_unref(plugin); + return std::all_of( + settings.check_elements.begin(), + settings.check_elements.end(), + [settings, gpu_vendor](const auto &el_name) { + // Can we instantiate the plugin? + if (auto el = gst_element_factory_make(el_name.c_str(), nullptr)) { + gst_object_unref(el); + // Is the selected GPU vendor compatible with the encoder? + // (Particularly useful when using multiple GPUs, e.g. nvcodec might be available but user + // wants to encode using the Intel GPU) + auto encoder_vendor = encoder_type(settings); + if (encoder_vendor == NVIDIA && gpu_vendor != GPU_VENDOR::NVIDIA) { + logs::log(logs::debug, "Skipping NVIDIA encoder, not a NVIDIA GPU ({})", (int)gpu_vendor); + return false; + } else if (encoder_vendor == VAAPI && (gpu_vendor != GPU_VENDOR::INTEL && gpu_vendor != GPU_VENDOR::AMD)) { + logs::log(logs::debug, "Skipping VAAPI encoder, not an Intel or AMD GPU ({})", (int)gpu_vendor); + return false; + } else if (encoder_vendor == QUICKSYNC && gpu_vendor != GPU_VENDOR::INTEL) { + logs::log(logs::debug, "Skipping QUICKSYNC encoder, not an Intel GPU ({})", (int)gpu_vendor); + return false; + } + return true; + } else { + return false; + } + }); + } + return false; } -toml::value v2_to_v3(const toml::value &v2, const std::string &source) { - auto v3 = toml::parse(source); - v3["uuid"] = v2.at("uuid").as_string(); - v3["config_version"] = 3; - v3["gstreamer"]["video"]["default_sink"] = - "rtpmoonlightpay_video name=moonlight_pay\n" - "payload_size={payload_size} fec_percentage={fec_percentage} " - "min_required_fec_packets={min_required_fec_packets} !\n" - "udpsink bind-port={host_port} host={client_ip} port={client_port} sync=true"; - v3["gstreamer"]["audio"]["default_sink"] = - "rtpmoonlightpay_audio name=moonlight_pay packet_duration={packet_duration} encrypt={encrypt}\n" - "aes_key=\"{aes_key}\" aes_iv=\"{aes_iv}\" !\n" - "udpsink bind-port={host_port} host={client_ip} port={client_port} sync=true"; - write(v3, source); - return v3; +std::optional +get_encoder(std::string_view tech, const std::vector &encoders, const GPU_VENDOR &vendor) { + auto default_is_available = std::bind(is_available, vendor, std::placeholders::_1); + auto encoder = std::find_if(encoders.begin(), encoders.end(), default_is_available); + if (encoder != std::end(encoders)) { + logs::log(logs::info, "Using {} encoder: {}", tech, encoder->plugin_name); + if (encoder_type(*encoder) == SOFTWARE) { + logs::log(logs::warning, "Software {} encoder detected", tech); + } + return *encoder; + } + return std::nullopt; +} + +toml::value v3_to_v4(const toml::value &v3, const std::string &source) { + std::filesystem::rename(source, source + ".v3.old"); + create_default(source); + auto v4 = toml::parse(source); + // Copy back everything apart from the Gstreamer pipelines + v4["hostname"] = v3.at("hostname").as_string(); + v4["uuid"] = v3.at("uuid").as_string(); + v4["apps"] = v3.at("apps"); + v4["paired_clients"] = v3.at("paired_clients"); + write(v4, source); + return v4; } Config load_or_default(const std::string &source, const std::shared_ptr &ev_bus) { @@ -188,18 +207,11 @@ Config load_or_default(const std::string &source, const std::shared_ptr(source); + auto cfg = toml::parse(source); auto version = toml::find_or(cfg, "config_version", 2); - if (version <= 1) { - logs::log(logs::warning, "Found old config file, migrating to newer version"); - cfg = v1_to_v2(cfg, source); - version = 2; - } - - if (version <= 2) { + if (version <= 3) { logs::log(logs::warning, "Found old config file, migrating to newer version"); - cfg = v2_to_v3(cfg, source); - version = 3; + cfg = v3_to_v4(cfg, source); } std::string uuid; @@ -213,41 +225,24 @@ Config load_or_default(const std::string &source, const std::shared_ptr(cfg, "support_av1", false); - - GstVideoCfg default_gst_video_settings = toml::find(cfg, "gstreamer", "video"); - GstAudioCfg default_gst_audio_settings = toml::find(cfg, "gstreamer", "audio"); - - /* Automatic pick best H264 encoder */ - auto h264_encoder = std::find_if(default_gst_video_settings.h264_encoders.begin(), - default_gst_video_settings.h264_encoders.end(), - is_available); - if (h264_encoder == std::end(default_gst_video_settings.h264_encoders)) { - throw std::runtime_error("Unable to find a compatible H264 encoder, please check [[gstreamer.video.h264_encoders]] " - "in your config.toml or your Gstreamer installation"); - } - logs::log(logs::info, "Selected H264 encoder: {}", h264_encoder->plugin_name); - - /* Automatic pick best HEVC encoder */ - auto hevc_encoder = std::find_if(default_gst_video_settings.hevc_encoders.begin(), - default_gst_video_settings.hevc_encoders.end(), - is_available); - if (hevc_encoder == std::end(default_gst_video_settings.hevc_encoders)) { - throw std::runtime_error("Unable to find a compatible HEVC encoder, please check [[gstreamer.video.hevc_encoders]] " - "in your config.toml or your Gstreamer installation"); - } - logs::log(logs::info, "Selected HEVC encoder: {}", hevc_encoder->plugin_name); - - /* Automatic pick best AV1 encoder */ - auto av1_encoder = std::find_if(default_gst_video_settings.av1_encoders.begin(), - default_gst_video_settings.av1_encoders.end(), - is_available); - if (support_av1 && av1_encoder == std::end(default_gst_video_settings.av1_encoders)) { - throw std::runtime_error("Unable to find a compatible AV1 encoder, please check [[gstreamer.video.av1_encoders]] " - "in your config.toml or your Gstreamer installation"); - } else if (support_av1) { - logs::log(logs::info, "Selected AV1 encoder: {}", av1_encoder->plugin_name); + + auto default_gst_video_settings = toml::find(cfg, "gstreamer", "video"); + auto default_gst_audio_settings = toml::find(cfg, "gstreamer", "audio"); + auto default_gst_encoder_settings = default_gst_video_settings.defaults; + + auto default_app_render_node = utils::get_env("WOLF_RENDER_NODE", "/dev/dri/renderD128"); + auto default_gst_render_node = utils::get_env("WOLF_ENCODER_NODE", default_app_render_node); + auto vendor = get_vendor(default_gst_render_node); + + /* Automatic pick best encoders */ + auto h264_encoder = get_encoder("H264", default_gst_video_settings.h264_encoders, vendor); + if (!h264_encoder) { + throw std::runtime_error( + "Unable to find a compatible H.264 encoder, please check [[gstreamer.video.h264_encoders]] " + "in your config.toml or your Gstreamer installation"); } + auto hevc_encoder = get_encoder("HEVC", default_gst_video_settings.hevc_encoders, vendor); + auto av1_encoder = get_encoder("AV1", default_gst_video_settings.av1_encoders, vendor); /* Get paired clients */ auto cfg_clients = toml::find>(cfg, "paired_clients"); @@ -256,7 +251,6 @@ Config load_or_default(const std::string &source, const std::shared_ptr{client}; }) // | ranges::to>>(); - std::string default_app_render_node = utils::get_env("WOLF_RENDER_NODE", "/dev/dri/renderD128"); /* Get apps, here we'll merge the default gstreamer settings with the app specific overrides */ auto cfg_apps = toml::find>(cfg, "apps"); auto apps = @@ -264,29 +258,55 @@ Config load_or_default(const std::string &source, const std::shared_ptr 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", h264_encoder->video_params) + - " ! " + toml::find_or(item, "video", "h264_encoder", h264_encoder->encoder_pipeline) + - " ! " + - 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", hevc_encoder->video_params) + - " ! " + toml::find_or(item, "video", "hevc_encoder", hevc_encoder->encoder_pipeline) + - " ! " + - toml::find_or(item, "video", " sink ", default_gst_video_settings.default_sink); + auto app_title = toml::find(item, "title"); + auto app_render_node = toml::find_or(item, "render_node", default_app_render_node); + if (app_render_node != default_gst_render_node) { + logs::log(logs::warning, + "App {} render node ({}) doesn't match the default GPU ({})", + app_title, + app_render_node, + default_gst_render_node); + // TODO: allow user to override gst_render_node + } + + auto h264_gst_pipeline = + toml::find_or(item, "video", "source", default_gst_video_settings.default_source) + " !\n" + + toml::find_or(item, + "video", + "video_params", + h264_encoder->video_params.value_or( + default_gst_encoder_settings.at(h264_encoder->plugin_name).video_params)) + + " !\n" + toml::find_or(item, "video", "h264_encoder", h264_encoder->encoder_pipeline) + " !\n" + + toml::find_or(item, "video", " sink ", default_gst_video_settings.default_sink); + + auto hevc_gst_pipeline = + hevc_encoder.has_value() + ? toml::find_or(item, "video", "source", default_gst_video_settings.default_source) + " !\n" + + toml::find_or(item, + "video", + "video_params", + hevc_encoder->video_params.value_or( + default_gst_encoder_settings.at(hevc_encoder->plugin_name).video_params)) + + " !\n" + toml::find_or(item, "video", "hevc_encoder", hevc_encoder->encoder_pipeline) + " !\n" + + toml::find_or(item, "video", " sink ", default_gst_video_settings.default_sink) + : ""; auto av1_gst_pipeline = - support_av1 ? toml::find_or(item, "video", "source", default_gst_video_settings.default_source) + " ! " + - toml::find_or(item, "video", "video_params", av1_encoder->video_params) + " ! " + - toml::find_or(item, "video", "av1_encoder", av1_encoder->encoder_pipeline) + " ! " + - toml::find_or(item, "video", " sink ", default_gst_video_settings.default_sink) - : ""; + av1_encoder.has_value() + ? toml::find_or(item, "video", "source", default_gst_video_settings.default_source) + " !\n" + + toml::find_or(item, + "video", + "video_params", + av1_encoder->video_params.value_or( + default_gst_encoder_settings.at(av1_encoder->plugin_name).video_params)) + + " !\n" + toml::find_or(item, "video", "av1_encoder", av1_encoder->encoder_pipeline) + " !\n" + + 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", "source", default_gst_audio_settings.default_source) + " !\n" + + toml::find_or(item, "audio", "video_params", default_gst_audio_settings.default_audio_params) + " !\n" + + toml::find_or(item, "audio", "opus_encoder", default_gst_audio_settings.default_opus_encoder) + " !\n" + toml::find_or(item, "audio", "sink", default_gst_audio_settings.default_sink); auto joypad_type = utils::to_lower(toml::find_or(item, "joypad_type", "auto"s)); @@ -301,16 +321,13 @@ Config load_or_default(const std::string &source, const std::shared_ptr(item, "title"), + return state::App{.base = {.title = app_title, .id = std::to_string(idx + 1), .support_hdr = toml::find_or(item, "support_hdr", false)}, .h264_gst_pipeline = h264_gst_pipeline, - .h264_encoder = encoder_type(h264_encoder->plugin_name), .hevc_gst_pipeline = hevc_gst_pipeline, - .hevc_encoder = encoder_type(hevc_encoder->plugin_name), .av1_gst_pipeline = av1_gst_pipeline, - .av1_encoder = support_av1 ? encoder_type(av1_encoder->plugin_name) : UNKNOWN, - .render_node = toml::find_or(item, "render_node", default_app_render_node), + .render_node = app_render_node, .opus_gst_pipeline = opus_gst_pipeline, .start_virtual_compositor = toml::find_or(item, "start_virtual_compositor", true), @@ -323,8 +340,8 @@ Config load_or_default(const std::string &source, const std::shared_ptr(cfg, "support_hevc", false), - .support_av1 = support_av1, + .support_hevc = hevc_encoder.has_value(), + .support_av1 = av1_encoder.has_value() && encoder_type(*av1_encoder) != SOFTWARE, .paired_clients = *clients_atom, .apps = apps}; } @@ -335,7 +352,7 @@ void pair(const Config &cfg, const PairedClient &client) { [&client](const state::PairedClientList &paired_clients) { return paired_clients.push_back(client); }); // Update TOML - toml::value tml = toml::parse(cfg.config_source); + toml::value tml = toml::parse(cfg.config_source); tml.at("paired_clients").as_array().emplace_back(client); write(tml, cfg.config_source); @@ -352,7 +369,7 @@ void unpair(const Config &cfg, const PairedClient &client) { }); // Update TOML - toml::value tml = toml::parse(cfg.config_source); + toml::value tml = toml::parse(cfg.config_source); auto &saved_clients = tml.at("paired_clients").as_array(); saved_clients.erase(std::remove_if(saved_clients.begin(), diff --git a/src/moonlight-server/state/data-structures.hpp b/src/moonlight-server/state/data-structures.hpp index 170fa2aa..5b4f4df5 100644 --- a/src/moonlight-server/state/data-structures.hpp +++ b/src/moonlight-server/state/data-structures.hpp @@ -102,11 +102,8 @@ struct App { moonlight::App base; std::string h264_gst_pipeline; - Encoder h264_encoder; std::string hevc_gst_pipeline; - Encoder hevc_encoder; std::string av1_gst_pipeline; - Encoder av1_encoder; std::string render_node; @@ -138,32 +135,12 @@ struct Config { immer::vector apps; }; -struct AudioMode { - - enum Speakers { - FRONT_LEFT, - FRONT_RIGHT, - FRONT_CENTER, - LOW_FREQUENCY, - BACK_LEFT, - BACK_RIGHT, - SIDE_LEFT, - SIDE_RIGHT, - MAX_SPEAKERS, - }; - - int channels{}; - int streams{}; - int coupled_streams{}; - immer::array speakers; -}; - /** * Host information like network, certificates and displays */ struct Host { immer::array display_modes; - immer::array audio_modes; + immer::array audio_modes; const X509 *server_cert; const EVP_PKEY *server_pkey; @@ -199,7 +176,7 @@ using JoypadList = immer::map event_bus; std::shared_ptr app; @@ -267,4 +244,78 @@ struct AppState { SessionsAtoms running_sessions; }; +const static immer::array AUDIO_CONFIGURATIONS = { + // TODO: opusenc doesn't allow us to set `coupled_streams` and `streams` + // don't change these or Moonlight will not be able to decode audio + // https://gitlab.freedesktop.org/gstreamer/gstreamer/-/blob/1.24.6/subprojects/gst-plugins-base/ext/opus/gstopusenc.c#L661-666 + {// Stereo + {.channels = 2, + .streams = 1, + .coupled_streams = 1, + .speakers = {audio::AudioMode::FRONT_LEFT, audio::AudioMode::FRONT_RIGHT}, + .bitrate = 96000}, + // 5.1 + {.channels = 6, + .streams = 4, + .coupled_streams = 2, + .speakers = {audio::AudioMode::FRONT_LEFT, + audio::AudioMode::FRONT_RIGHT, + audio::AudioMode::FRONT_CENTER, + audio::AudioMode::LOW_FREQUENCY, + audio::AudioMode::BACK_LEFT, + audio::AudioMode::BACK_RIGHT}, + .bitrate = 256000}, + // 7.1 + {.channels = 8, + .streams = 5, + .coupled_streams = 3, + .speakers = {audio::AudioMode::FRONT_LEFT, + audio::AudioMode::FRONT_RIGHT, + audio::AudioMode::FRONT_CENTER, + audio::AudioMode::LOW_FREQUENCY, + audio::AudioMode::BACK_LEFT, + audio::AudioMode::BACK_RIGHT, + audio::AudioMode::SIDE_LEFT, + audio::AudioMode::SIDE_RIGHT}, + .bitrate = 450000}}}; + +static const audio::AudioMode &get_audio_mode(int channels, bool high_quality) { + int base_index = 0; + if (channels == 6) { + base_index = 2; + } else if (channels == 8) { + base_index = 4; + } + + return AUDIO_CONFIGURATIONS[base_index]; // TODO: add high quality settings, it sounds bad if we can't change the + // opusenc settings too.. +} + +/** + * Not many clients will actually look at this but the Nintendo Switch will flat out refuse to connect if the + * advertised display modes don't match + */ +const static immer::array DISPLAY_CONFIGURATIONS = {{ + // 720p + {.width = 1280, .height = 720, .refreshRate = 120}, + {.width = 1280, .height = 720, .refreshRate = 60}, + {.width = 1280, .height = 720, .refreshRate = 30}, + // 1080p + {.width = 1920, .height = 1080, .refreshRate = 120}, + {.width = 1920, .height = 1080, .refreshRate = 60}, + {.width = 1920, .height = 1080, .refreshRate = 30}, + // 1440p + {.width = 2560, .height = 1440, .refreshRate = 120}, + {.width = 2560, .height = 1440, .refreshRate = 90}, + {.width = 2560, .height = 1440, .refreshRate = 60}, + // 2160p + {.width = 3840, .height = 2160, .refreshRate = 120}, + {.width = 3840, .height = 2160, .refreshRate = 90}, + {.width = 3840, .height = 2160, .refreshRate = 60}, + // 8k + {.width = 7680, .height = 4320, .refreshRate = 120}, + {.width = 7680, .height = 4320, .refreshRate = 90}, + {.width = 7680, .height = 4320, .refreshRate = 60}, +}}; + } // namespace state \ No newline at end of file diff --git a/src/moonlight-server/state/default/config.include.toml b/src/moonlight-server/state/default/config.include.toml index 3be13203..e1f8768c 100644 --- a/src/moonlight-server/state/default/config.include.toml +++ b/src/moonlight-server/state/default/config.include.toml @@ -1,18 +1,13 @@ R"for_c++_include( # The name that will be displayed in Moonlight hostname = "Wolf" -# Set to false if this host doesn't support HEVC -support_hevc = true -# Set to true if this host supports AV1 (EXPERIMENTAL) -support_av1 = false # The version of this config file -config_version = 3 - - +config_version = 4 # A list of paired clients that will be allowed to stream paired_clients = [] - +###################### +# Apps, the list of apps that will be shown in Moonlight [[apps]] title = "Firefox" start_virtual_compositor = true @@ -27,8 +22,7 @@ env = [ "MOZ_ENABLE_WAYLAND=1", "GOW_REQUIRED_DEVICES=/dev/input/* /dev/dri/* /dev/nvidia*", ] -devices = [ -] +devices = [] ports = [] base_create_json = """ { @@ -55,8 +49,7 @@ env = [ "RUN_SWAY=true", "GOW_REQUIRED_DEVICES=/dev/input/* /dev/dri/* /dev/nvidia*", ] -devices = [ -] +devices = [] ports = [] base_create_json = """ { @@ -84,8 +77,7 @@ env = [ "RUN_SWAY=true", "GOW_REQUIRED_DEVICES=/dev/input/* /dev/dri/* /dev/nvidia*", ] -devices = [ -] +devices = [] ports = [] base_create_json = """ { @@ -114,8 +106,7 @@ env = [ "RUN_SWAY=1", "GOW_REQUIRED_DEVICES=/dev/input/event* /dev/dri/* /dev/nvidia*", ] -devices = [ -] +devices = [] ports = [] base_create_json = """ { @@ -147,17 +138,43 @@ video/x-raw, framerate={fps}/1 [apps.audio] source = "audiotestsrc wave=ticks is-live=true" - +###################### +# Gstreamer: Video/Audio encoding pipelines and streaming settings [gstreamer] [gstreamer.video] default_source = "appsrc name=wolf_wayland_source is-live=true block=false format=3 stream-type=0" default_sink = """ -rtpmoonlightpay_video name=moonlight_pay +rtpmoonlightpay_video name=moonlight_pay \ payload_size={payload_size} fec_percentage={fec_percentage} min_required_fec_packets={min_required_fec_packets} ! -udpsink bind-port={host_port} host={client_ip} port={client_port} sync=true -\ +udpsink bind-port={host_port} host={client_ip} port={client_port} sync=true\ +""" + +###################### +# Default settings for the main encoders +# To avoid repetition between H264, HEVC and AV1 encoders +[gstreamer.video.defaults.nvcodec] +video_params = """ +queue ! +cudaupload ! +cudaconvertscale ! +video/x-raw(memory:CUDAMemory), width={width}, height={height}, \ +chroma-site={color_range}, format=NV12, colorimetry={color_space}, pixel-aspect-ratio=1/1\ +""" + +[gstreamer.video.defaults.qsv] +video_params = """ +queue ! +videoconvert ! +video/x-raw, chroma-site={color_range}, width={width}, height={height}, format=NV12, colorimetry={color_space}\ +""" + +[gstreamer.video.defaults.vaapi] +video_params = """ +queue ! +videoconvert ! +video/x-raw, chroma-site={color_range}, width={width}, height={height}, format=NV12, colorimetry={color_space}\ """ ###################### @@ -165,71 +182,39 @@ udpsink bind-port={host_port} host={client_ip} port={client_port} sync=true # Order here matters: Wolf will try them in order and pick the first one that's not failing ### [[gstreamer.video.hevc_encoders]] -plugin_name = "nvcodec" # Nvidia +plugin_name = "nvcodec" check_elements = ["nvh265enc", "cudaconvertscale", "cudaupload"] -video_params = """ -queue ! cudaupload ! cudaconvertscale ! -video/x-raw(memory:CUDAMemory), width={width}, height={height}, -chroma-site={color_range}, format=NV12, colorimetry={color_space}, pixel-aspect-ratio=1/1 -\ -""" encoder_pipeline = """ -nvh265enc preset=low-latency-hq zerolatency=true gop-size=-1 rc-mode=cbr-ld-hq bitrate={bitrate} aud=false ! +nvh265enc gop-size=-1 bitrate={bitrate} aud=false rc-mode=cbr zerolatency=true preset=p1 tune=ultra-low-latency multi-pass=two-pass-quarter ! h265parse ! -video/x-h265, profile=main, stream-format=byte-stream -\ +video/x-h265, profile=main, stream-format=byte-stream\ """ [[gstreamer.video.hevc_encoders]] -plugin_name = "qsv" # Intel QuickSync -check_elements = ["qsvh265enc", "vapostproc"] -video_params = """ -queue ! -vapostproc ! -video/x-raw(memory:VAMemory), chroma-site={color_range}, width={width}, -height={height}, format=NV12, colorimetry={color_space} -\ -""" +plugin_name = "qsv" +check_elements = ["qsvh265enc", "videoconvert"] encoder_pipeline = """ -qsvh265enc b-frames=0 gop-size=0 idr-interval=1 ref-frames=1 max-bitrate={bitrate} rate-control=qvbr ! +qsvh265enc b-frames=0 gop-size=0 idr-interval=1 ref-frames=1 bitrate={bitrate} rate-control=cbr low-latency=1 target-usage=6 ! h265parse ! -video/x-h265, profile=main, stream-format=byte-stream -\ +video/x-h265, profile=main, stream-format=byte-stream\ """ [[gstreamer.video.hevc_encoders]] -plugin_name = "vaapi" # VAAPI: Intel/AMD -check_elements = ["vah265enc", "vapostproc"] -video_params = """ -queue ! -vapostproc ! -video/x-raw(memory:VAMemory), chroma-site={color_range}, width={width}, -height={height}, format=NV12, colorimetry={color_space} -\ -""" +plugin_name = "vaapi" +check_elements = ["vah265lpenc", "videoconvert"] # lp: (Low Power) encoder_pipeline = """ -vah265enc aud=false b-frames=0 ref-frames=1 num-slices={slices_per_frame} bitrate={bitrate} ! +vah265lpenc aud=false b-frames=0 ref-frames=1 num-slices={slices_per_frame} bitrate={bitrate} mbbrc=1 rate-control=cbr target-usage=6 ! h265parse ! -video/x-h265, profile=main, stream-format=byte-stream -\ +video/x-h265, profile=main, stream-format=byte-stream\ """ [[gstreamer.video.hevc_encoders]] -plugin_name = "applemedia" # OSX HW encoder -video_params = """ -videoscale ! -videoconvert ! -videorate ! -video/x-raw, width={width}, height={height}, framerate={fps}/1, format=I420, -chroma-site={color_range}, colorimetry={color_space} -\ -""" -check_elements = ["vtenc_h265_hw"] +plugin_name = "vaapi" +check_elements = ["vah265enc", "videoconvert"] encoder_pipeline = """ -vtenc_h265_hw allow-frame-reordering=false bitrate={bitrate} max-keyframe-interval=-1 realtime=true ! +vah265enc aud=false b-frames=0 ref-frames=1 num-slices={slices_per_frame} bitrate={bitrate} mbbrc=1 rate-control=cbr target-usage=6 ! h265parse ! -video/x-h265, profile=main, stream-format=byte-stream -\ +video/x-h265, profile=main, stream-format=byte-stream\ """ [[gstreamer.video.hevc_encoders]] @@ -240,14 +225,12 @@ videoscale ! videoconvert ! videorate ! video/x-raw, width={width}, height={height}, framerate={fps}/1, format=I420, -chroma-site={color_range}, colorimetry={color_space} -\ +chroma-site={color_range}, colorimetry={color_space}\ """ encoder_pipeline = """ x265enc tune=zerolatency speed-preset=superfast bitrate={bitrate} option-string="info=0:keyint=-1:qp=28:repeat-headers=1:slices={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 -\ +video/x-h265, profile=main, stream-format=byte-stream\ """ @@ -256,71 +239,39 @@ video/x-h265, profile=main, stream-format=byte-stream # Order here matters: Wolf will try them in order and pick the first one that's not failing ### [[gstreamer.video.h264_encoders]] -plugin_name = "nvcodec" # Nvidia +plugin_name = "nvcodec" check_elements = ["nvh264enc", "cudaconvertscale", "cudaupload"] -video_params = """ -queue ! cudaupload ! cudaconvertscale ! -video/x-raw(memory:CUDAMemory), width={width}, height={height}, -chroma-site={color_range}, format=NV12, colorimetry={color_space}, pixel-aspect-ratio=1/1 -\ -""" encoder_pipeline = """ nvh264enc preset=low-latency-hq zerolatency=true gop-size=0 rc-mode=cbr-ld-hq bitrate={bitrate} aud=false ! h264parse ! -video/x-h264, profile=main, stream-format=byte-stream -\ +video/x-h264, profile=main, stream-format=byte-stream\ """ [[gstreamer.video.h264_encoders]] -plugin_name = "qsv" # Intel QuickSync -check_elements = ["qsvh264enc", "vapostproc"] -video_params = """ -queue ! -vapostproc ! -video/x-raw(memory:VAMemory), chroma-site={color_range}, width={width}, -height={height}, format=NV12, colorimetry={color_space} -\ -""" +plugin_name = "qsv" +check_elements = ["qsvh264enc", "videoconvert"] encoder_pipeline = """ -qsvh264enc b-frames=0 gop-size=0 idr-interval=1 ref-frames=1 max-bitrate={bitrate} rate-control=qvbr ! +qsvh264enc b-frames=0 gop-size=0 idr-interval=1 ref-frames=1 bitrate={bitrate} rate-control=cbr target-usage=6 ! h264parse ! -video/x-h264, profile=main, stream-format=byte-stream -\ +video/x-h264, profile=main, stream-format=byte-stream\ """ [[gstreamer.video.h264_encoders]] -plugin_name = "vaapi" # VAAPI: Intel/AMD -check_elements = ["vah264enc", "vapostproc"] -video_params = """ -queue ! -vapostproc ! -video/x-raw(memory:VAMemory), chroma-site={color_range}, width={width}, -height={height}, format=NV12, colorimetry={color_space} -\ -""" +plugin_name = "vaapi" +check_elements = ["vah264lpenc", "videoconvert"] # lp: (Low Power) encoder_pipeline = """ -vah264enc aud=false b-frames=0 ref-frames=1 num-slices={slices_per_frame} bitrate={bitrate} ! +vah264lpenc aud=false b-frames=0 ref-frames=1 num-slices={slices_per_frame} bitrate={bitrate} target-usage=6 ! h264parse ! -video/x-h264, profile=main, stream-format=byte-stream -\ +video/x-h264, profile=main, stream-format=byte-stream\ """ [[gstreamer.video.h264_encoders]] -plugin_name = "applemedia" # OSX HW encoder -video_params = """ -videoscale ! -videoconvert ! -videorate ! -video/x-raw, width={width}, height={height}, framerate={fps}/1, format=I420, -chroma-site={color_range}, colorimetry={color_space} -\ -""" -check_elements = ["vtenc_h264_hw"] +plugin_name = "vaapi" +check_elements = ["vah264enc", "videoconvert"] encoder_pipeline = """ -vtenc_h264_hw allow-frame-reordering=false bitrate={bitrate} max-keyframe-interval=-1 realtime=true ! +vah264enc aud=false b-frames=0 ref-frames=1 num-slices={slices_per_frame} bitrate={bitrate} target-usage=6 ! h264parse ! -video/x-h264, profile=main, stream-format=byte-stream -\ +video/x-h264, profile=main, stream-format=byte-stream\ """ [[gstreamer.video.h264_encoders]] @@ -331,20 +282,54 @@ videoscale ! videoconvert ! videorate ! video/x-raw, width={width}, height={height}, framerate={fps}/1, format=I420, -chroma-site={color_range}, colorimetry={color_space} -\ +chroma-site={color_range}, colorimetry={color_space}\ """ encoder_pipeline = """ x264enc pass=qual tune=zerolatency speed-preset=superfast b-adapt=false bframes=0 ref=1 sliced-threads=true threads={slices_per_frame} option-string="slices={slices_per_frame}:keyint=infinite:open-gop=0" b-adapt=false bitrate={bitrate} aud=false ! -video/x-h264, profile=high, stream-format=byte-stream -\ +video/x-h264, profile=high, stream-format=byte-stream\ """ ############## # AV1 encoders ### +[[gstreamer.video.av1_encoders]] +plugin_name = "nvcodec" +check_elements = ["nvav1enc", "cudaconvertscale", "cudaupload"] +encoder_pipeline = """ +nvav1enc gop-size=-1 bitrate={bitrate} rc-mode=cbr zerolatency=true preset=p1 tune=ultra-low-latency multi-pass=two-pass-quarter ! +av1parse ! +video/x-av1, stream-format=obu-stream, alignment=frame, profile=main\ +""" + +[[gstreamer.video.av1_encoders]] +plugin_name = "qsv" +check_elements = ["qsvav1enc", "videoconvert"] +encoder_pipeline = """ +qsvav1enc gop-size=0 ref-frames=1 bitrate={bitrate} rate-control=cbr low-latency=1 target-usage=6 ! +av1parse ! +video/x-av1, stream-format=obu-stream, alignment=frame, profile=main\ +""" + +[[gstreamer.video.av1_encoders]] +plugin_name = "vaapi" +check_elements = ["vaav1enc", "videoconvert"] +encoder_pipeline = """ +vaav1enc gop-size=0 ref-frames=1 bitrate={bitrate} rate-control=cbr ! +av1parse ! +video/x-av1, stream-format=obu-stream, alignment=frame, profile=main\ +""" + +[[gstreamer.video.av1_encoders]] +plugin_name = "vaapi" +check_elements = ["vaav1lpenc", "videoconvert"] # LP = Low Power +encoder_pipeline = """ +vaav1lpenc gop-size=0 ref-frames=1 bitrate={bitrate} rate-control=cbr ! +av1parse ! +video/x-av1, stream-format=obu-stream, alignment=frame, profile=main\ +""" + [[gstreamer.video.av1_encoders]] plugin_name = "aom" check_elements = ["av1enc"] @@ -353,14 +338,12 @@ videoscale ! videoconvert ! videorate ! video/x-raw, width={width}, height={height}, framerate={fps}/1, format=I420, -chroma-site={color_range}, colorimetry={color_space} -\ +chroma-site={color_range}, colorimetry={color_space}\ """ encoder_pipeline = """ av1enc usage-profile=realtime end-usage=vbr target-bitrate={bitrate} ! av1parse ! -video/x-av1, stream-format=obu-stream, alignment=frame, profile=main -\ +video/x-av1, stream-format=obu-stream, alignment=frame, profile=main\ """ ########### @@ -368,23 +351,20 @@ video/x-av1, stream-format=obu-stream, alignment=frame, profile=main ### [gstreamer.audio] default_source = """ -pulsesrc device="{sink_name}" server="{server_name}" -\ +pulsesrc device="{sink_name}" server="{server_name}"\ """ -default_audio_params = "audio/x-raw, channels={channels}" +default_audio_params = "audio/x-raw, channels={channels}, rate=48000" default_opus_encoder = """ -opusenc bitrate={bitrate} bitrate-type=constrained-vbr frame-size={packet_duration} bandwidth=fullband -audio-type=restricted-lowdelay max-payload-size=1400 -\ +opusenc bitrate={bitrate} bitrate-type=cbr frame-size={packet_duration} bandwidth=fullband \ +audio-type=restricted-lowdelay max-payload-size=1400\ """ default_sink = """ -rtpmoonlightpay_audio name=moonlight_pay packet_duration={packet_duration} encrypt={encrypt} -aes_key="{aes_key}" aes_iv="{aes_iv}" ! -udpsink bind-port={host_port} host={client_ip} port={client_port} sync=true -\ +rtpmoonlightpay_audio name=moonlight_pay packet_duration={packet_duration} encrypt={encrypt} \ +aes_key="{aes_key}" aes_iv="{aes_iv}" ! +udpsink bind-port={host_port} host={client_ip} port={client_port} sync=true\ """ -)for_c++_include" +)for_c++_include" \ No newline at end of file diff --git a/src/moonlight-server/state/default/config.v2.toml b/src/moonlight-server/state/default/config.v3.toml similarity index 99% rename from src/moonlight-server/state/default/config.v2.toml rename to src/moonlight-server/state/default/config.v3.toml index 71441b4f..84c54016 100644 --- a/src/moonlight-server/state/default/config.v2.toml +++ b/src/moonlight-server/state/default/config.v3.toml @@ -385,4 +385,3 @@ aes_key="{aes_key}" aes_iv="{aes_iv}" ! udpsink bind-port={host_port} host={client_ip} port={client_port} sync=true \ """ - diff --git a/src/moonlight-server/state/default/config.v4.toml b/src/moonlight-server/state/default/config.v4.toml new file mode 100644 index 00000000..d58a636e --- /dev/null +++ b/src/moonlight-server/state/default/config.v4.toml @@ -0,0 +1,368 @@ +# The name that will be displayed in Moonlight +hostname = "Wolf" +# The version of this config file +config_version = 4 +# A list of paired clients that will be allowed to stream +paired_clients = [] + +###################### +# Apps, the list of apps that will be shown in Moonlight +[[apps]] +title = "Firefox" +start_virtual_compositor = true + +[apps.runner] +type = "docker" +name = "WolfFirefox" +image = "ghcr.io/games-on-whales/firefox:edge" +mounts = [] +env = [ + "RUN_SWAY=1", + "MOZ_ENABLE_WAYLAND=1", + "GOW_REQUIRED_DEVICES=/dev/input/* /dev/dri/* /dev/nvidia*", +] +devices = [] +ports = [] +base_create_json = """ +{ + "HostConfig": { + "IpcMode": "host", + "Privileged": false, + "CapAdd": ["NET_RAW", "MKNOD", "NET_ADMIN"], + "DeviceCgroupRules": ["c 13:* rmw", "c 244:* rmw"] + } +} +\ +""" + +[[apps]] +title = "RetroArch" +start_virtual_compositor = true + +[apps.runner] +type = "docker" +name = "WolfRetroarch" +image = "ghcr.io/games-on-whales/retroarch:edge" +mounts = [] +env = [ + "RUN_SWAY=true", + "GOW_REQUIRED_DEVICES=/dev/input/* /dev/dri/* /dev/nvidia*", +] +devices = [] +ports = [] +base_create_json = """ +{ + "HostConfig": { + "IpcMode": "host", + "CapAdd": ["NET_RAW", "MKNOD", "NET_ADMIN", "SYS_ADMIN", "SYS_NICE"], + "Privileged": false, + "DeviceCgroupRules": ["c 13:* rmw", "c 244:* rmw"] + } +} +\ +""" + +[[apps]] +title = "Steam" +start_virtual_compositor = true + +[apps.runner] +type = "docker" +name = "WolfSteam" +image = "ghcr.io/games-on-whales/steam:edge" +mounts = [] +env = [ + "PROTON_LOG=1", + "RUN_SWAY=true", + "GOW_REQUIRED_DEVICES=/dev/input/* /dev/dri/* /dev/nvidia*", +] +devices = [] +ports = [] +base_create_json = """ +{ + "HostConfig": { + "IpcMode": "host", + "CapAdd": ["SYS_ADMIN", "SYS_NICE", "SYS_PTRACE", "NET_RAW", "MKNOD", "NET_ADMIN"], + "SecurityOpt": ["seccomp=unconfined", "apparmor=unconfined"], + "Ulimits": [{"Name":"nofile", "Hard":10240, "Soft":10240}], + "Privileged": false, + "DeviceCgroupRules": ["c 13:* rmw", "c 244:* rmw"] + } +} +\ +""" + +[[apps]] +title = "Pegasus" +start_virtual_compositor = true + +[apps.runner] +type = "docker" +name = "WolfPegasus" +image = "ghcr.io/games-on-whales/pegasus:edge" +mounts = [] +env = [ + "RUN_SWAY=1", + "GOW_REQUIRED_DEVICES=/dev/input/event* /dev/dri/* /dev/nvidia*", +] +devices = [] +ports = [] +base_create_json = """ +{ + "HostConfig": { + "IpcMode": "host", + "CapAdd": ["NET_RAW", "MKNOD", "NET_ADMIN", "SYS_ADMIN", "SYS_NICE"], + "Privileged": false, + "DeviceCgroupRules": ["c 13:* rmw", "c 244:* rmw"] + } +} +\ +""" + +[[apps]] +title = "Test ball" +start_virtual_compositor = false + +[apps.runner] +type = "process" +run_cmd = "sh -c \"while :; do echo 'running...'; sleep 10; done\"" + +[apps.video] +source = """ +videotestsrc pattern=ball flip=true is-live=true ! +video/x-raw, framerate={fps}/1 +\ +""" + +[apps.audio] +source = "audiotestsrc wave=ticks is-live=true" + +###################### +# Gstreamer: Video/Audio encoding pipelines and streaming settings +[gstreamer] + +[gstreamer.video] + +default_source = "appsrc name=wolf_wayland_source is-live=true block=false format=3 stream-type=0" +default_sink = """ +rtpmoonlightpay_video name=moonlight_pay \ +payload_size={payload_size} fec_percentage={fec_percentage} min_required_fec_packets={min_required_fec_packets} ! +udpsink bind-port={host_port} host={client_ip} port={client_port} sync=true\ +""" + +###################### +# Default settings for the main encoders +# To avoid repetition between H264, HEVC and AV1 encoders +[gstreamer.video.defaults.nvcodec] +video_params = """ +queue ! +cudaupload ! +cudaconvertscale ! +video/x-raw(memory:CUDAMemory), width={width}, height={height}, \ +chroma-site={color_range}, format=NV12, colorimetry={color_space}, pixel-aspect-ratio=1/1\ +""" + +[gstreamer.video.defaults.qsv] +video_params = """ +queue ! +videoconvert ! +video/x-raw, chroma-site={color_range}, width={width}, height={height}, format=NV12, colorimetry={color_space}\ +""" + +[gstreamer.video.defaults.vaapi] +video_params = """ +queue ! +videoconvert ! +video/x-raw, chroma-site={color_range}, width={width}, height={height}, format=NV12, colorimetry={color_space}\ +""" + +###################### +# HEVC encoders +# Order here matters: Wolf will try them in order and pick the first one that's not failing +### +[[gstreamer.video.hevc_encoders]] +plugin_name = "nvcodec" +check_elements = ["nvh265enc", "cudaconvertscale", "cudaupload"] +encoder_pipeline = """ +nvh265enc gop-size=-1 bitrate={bitrate} aud=false rc-mode=cbr zerolatency=true preset=p1 tune=ultra-low-latency multi-pass=two-pass-quarter ! +h265parse ! +video/x-h265, profile=main, stream-format=byte-stream\ +""" + +[[gstreamer.video.hevc_encoders]] +plugin_name = "qsv" +check_elements = ["qsvh265enc", "videoconvert"] +encoder_pipeline = """ +qsvh265enc b-frames=0 gop-size=0 idr-interval=1 ref-frames=1 bitrate={bitrate} rate-control=cbr low-latency=1 target-usage=6 ! +h265parse ! +video/x-h265, profile=main, stream-format=byte-stream\ +""" + +[[gstreamer.video.hevc_encoders]] +plugin_name = "vaapi" +check_elements = ["vah265lpenc", "videoconvert"] # lp: (Low Power) +encoder_pipeline = """ +vah265lpenc aud=false b-frames=0 ref-frames=1 num-slices={slices_per_frame} bitrate={bitrate} mbbrc=1 rate-control=cbr target-usage=6 ! +h265parse ! +video/x-h265, profile=main, stream-format=byte-stream\ +""" + +[[gstreamer.video.hevc_encoders]] +plugin_name = "vaapi" +check_elements = ["vah265enc", "videoconvert"] +encoder_pipeline = """ +vah265enc aud=false b-frames=0 ref-frames=1 num-slices={slices_per_frame} bitrate={bitrate} mbbrc=1 rate-control=cbr target-usage=6 ! +h265parse ! +video/x-h265, profile=main, stream-format=byte-stream\ +""" + +[[gstreamer.video.hevc_encoders]] +plugin_name = "x265" # SW Encoding +check_elements = ["x265enc"] +video_params = """ +videoscale ! +videoconvert ! +videorate ! +video/x-raw, width={width}, height={height}, framerate={fps}/1, format=I420, +chroma-site={color_range}, colorimetry={color_space}\ +""" +encoder_pipeline = """ +x265enc tune=zerolatency speed-preset=superfast bitrate={bitrate} +option-string="info=0:keyint=-1:qp=28:repeat-headers=1:slices={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\ +""" + + +###################### +# H264 encoders +# Order here matters: Wolf will try them in order and pick the first one that's not failing +### +[[gstreamer.video.h264_encoders]] +plugin_name = "nvcodec" +check_elements = ["nvh264enc", "cudaconvertscale", "cudaupload"] +encoder_pipeline = """ +nvh264enc preset=low-latency-hq zerolatency=true gop-size=0 rc-mode=cbr-ld-hq bitrate={bitrate} aud=false ! +h264parse ! +video/x-h264, profile=main, stream-format=byte-stream\ +""" + +[[gstreamer.video.h264_encoders]] +plugin_name = "qsv" +check_elements = ["qsvh264enc", "videoconvert"] +encoder_pipeline = """ +qsvh264enc b-frames=0 gop-size=0 idr-interval=1 ref-frames=1 bitrate={bitrate} rate-control=cbr target-usage=6 ! +h264parse ! +video/x-h264, profile=main, stream-format=byte-stream\ +""" + +[[gstreamer.video.h264_encoders]] +plugin_name = "vaapi" +check_elements = ["vah264lpenc", "videoconvert"] # lp: (Low Power) +encoder_pipeline = """ +vah264lpenc aud=false b-frames=0 ref-frames=1 num-slices={slices_per_frame} bitrate={bitrate} target-usage=6 ! +h264parse ! +video/x-h264, profile=main, stream-format=byte-stream\ +""" + +[[gstreamer.video.h264_encoders]] +plugin_name = "vaapi" +check_elements = ["vah264enc", "videoconvert"] +encoder_pipeline = """ +vah264enc aud=false b-frames=0 ref-frames=1 num-slices={slices_per_frame} bitrate={bitrate} target-usage=6 ! +h264parse ! +video/x-h264, profile=main, stream-format=byte-stream\ +""" + +[[gstreamer.video.h264_encoders]] +plugin_name = "x264" # SW Encoding +check_elements = ["x264enc"] +video_params = """ +videoscale ! +videoconvert ! +videorate ! +video/x-raw, width={width}, height={height}, framerate={fps}/1, format=I420, +chroma-site={color_range}, colorimetry={color_space}\ +""" +encoder_pipeline = """ +x264enc pass=qual tune=zerolatency speed-preset=superfast b-adapt=false bframes=0 ref=1 +sliced-threads=true threads={slices_per_frame} option-string="slices={slices_per_frame}:keyint=infinite:open-gop=0" +b-adapt=false bitrate={bitrate} aud=false ! +video/x-h264, profile=high, stream-format=byte-stream\ +""" + +############## +# AV1 encoders +### +[[gstreamer.video.av1_encoders]] +plugin_name = "nvcodec" +check_elements = ["nvav1enc", "cudaconvertscale", "cudaupload"] +encoder_pipeline = """ +nvav1enc gop-size=-1 bitrate={bitrate} rc-mode=cbr zerolatency=true preset=p1 tune=ultra-low-latency multi-pass=two-pass-quarter ! +av1parse ! +video/x-av1, stream-format=obu-stream, alignment=frame, profile=main\ +""" + +[[gstreamer.video.av1_encoders]] +plugin_name = "qsv" +check_elements = ["qsvav1enc", "videoconvert"] +encoder_pipeline = """ +qsvav1enc gop-size=0 ref-frames=1 bitrate={bitrate} rate-control=cbr low-latency=1 target-usage=6 ! +av1parse ! +video/x-av1, stream-format=obu-stream, alignment=frame, profile=main\ +""" + +[[gstreamer.video.av1_encoders]] +plugin_name = "vaapi" +check_elements = ["vaav1enc", "videoconvert"] +encoder_pipeline = """ +vaav1enc gop-size=0 ref-frames=1 bitrate={bitrate} rate-control=cbr ! +av1parse ! +video/x-av1, stream-format=obu-stream, alignment=frame, profile=main\ +""" + +[[gstreamer.video.av1_encoders]] +plugin_name = "vaapi" +check_elements = ["vaav1lpenc", "videoconvert"] # LP = Low Power +encoder_pipeline = """ +vaav1lpenc gop-size=0 ref-frames=1 bitrate={bitrate} rate-control=cbr ! +av1parse ! +video/x-av1, stream-format=obu-stream, alignment=frame, profile=main\ +""" + +[[gstreamer.video.av1_encoders]] +plugin_name = "aom" +check_elements = ["av1enc"] +video_params = """ +videoscale ! +videoconvert ! +videorate ! +video/x-raw, width={width}, height={height}, framerate={fps}/1, format=I420, +chroma-site={color_range}, colorimetry={color_space}\ +""" +encoder_pipeline = """ +av1enc usage-profile=realtime end-usage=vbr target-bitrate={bitrate} ! +av1parse ! +video/x-av1, stream-format=obu-stream, alignment=frame, profile=main\ +""" + +########### +# Audio +### +[gstreamer.audio] +default_source = """ +pulsesrc device="{sink_name}" server="{server_name}"\ +""" + +default_audio_params = "audio/x-raw, channels={channels}, rate=48000" + +default_opus_encoder = """ +opusenc bitrate={bitrate} bitrate-type=cbr frame-size={packet_duration} bandwidth=fullband \ +audio-type=restricted-lowdelay max-payload-size=1400\ +""" + +default_sink = """ +rtpmoonlightpay_audio name=moonlight_pay packet_duration={packet_duration} encrypt={encrypt} \ +aes_key="{aes_key}" aes_iv="{aes_iv}" ! +udpsink bind-port={host_port} host={client_ip} port={client_port} sync=true\ +""" + diff --git a/src/moonlight-server/streaming/data-structures.hpp b/src/moonlight-server/streaming/data-structures.hpp index 8b44e71c..44fa02b3 100644 --- a/src/moonlight-server/streaming/data-structures.hpp +++ b/src/moonlight-server/streaming/data-structures.hpp @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include #include @@ -40,7 +41,7 @@ struct VideoSession { int frames_with_invalid_ref_threshold; int fec_percentage; int min_required_fec_packets; - int bitrate_kbps; + long bitrate_kbps; int slices_per_frame; ColorRange color_range; @@ -49,6 +50,8 @@ struct VideoSession { std::string client_ip; }; +using namespace wolf::core::audio; + struct AudioSession { std::string gst_pipeline; @@ -63,8 +66,7 @@ struct AudioSession { std::string client_ip; int packet_duration; - int channels; - int bitrate = 48000; + AudioMode audio_mode; }; /** diff --git a/src/moonlight-server/streaming/streaming.cpp b/src/moonlight-server/streaming/streaming.cpp index 055ea5ee..1e78829e 100644 --- a/src/moonlight-server/streaming/streaming.cpp +++ b/src/moonlight-server/streaming/streaming.cpp @@ -113,7 +113,7 @@ void start_streaming_video(const immer::box &video_session, fmt::arg("color_space", color_space), fmt::arg("color_range", color_range), fmt::arg("host_port", video_session->port)); - logs::log(logs::debug, "Starting video pipeline: {}", pipeline); + logs::log(logs::debug, "Starting video pipeline: \n{}", pipeline); auto appsrc_state = custom_src::setup_app_src(video_session, std::move(wl_ptr)); @@ -202,19 +202,24 @@ void start_streaming_audio(const immer::box &audio_session, unsigned short client_port, const std::string &sink_name, const std::string &server_name) { - auto pipeline = fmt::format(audio_session->gst_pipeline, - fmt::arg("channels", audio_session->channels), - fmt::arg("bitrate", audio_session->bitrate), - fmt::arg("sink_name", sink_name), - fmt::arg("server_name", server_name), - 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("host_port", audio_session->port)); - logs::log(logs::debug, "Starting audio pipeline: {}", pipeline); + auto pipeline = fmt::format( + audio_session->gst_pipeline, + fmt::arg("channels", audio_session->audio_mode.channels), + fmt::arg("bitrate", audio_session->audio_mode.bitrate), + // TODO: opusenc hardcodes those two + // https://gitlab.freedesktop.org/gstreamer/gstreamer/-/blob/1.24.6/subprojects/gst-plugins-base/ext/opus/gstopusenc.c#L661-666 + fmt::arg("streams", audio_session->audio_mode.streams), + fmt::arg("coupled_streams", audio_session->audio_mode.coupled_streams), + fmt::arg("sink_name", sink_name), + fmt::arg("server_name", server_name), + 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("host_port", audio_session->port)); + logs::log(logs::debug, "Starting audio pipeline: \n{}", pipeline); run_pipeline(pipeline, [session_id = audio_session->session_id, event_bus](auto pipeline, auto loop) { auto pause_handler = event_bus->register_handler>( diff --git a/src/moonlight-server/wolf.cpp b/src/moonlight-server/wolf.cpp index 169a0933..cc0e7a79 100644 --- a/src/moonlight-server/wolf.cpp +++ b/src/moonlight-server/wolf.cpp @@ -35,21 +35,6 @@ auto load_config(std::string_view config_file, const std::shared_ptr getDisplayModes() { - return {{1920, 1080, 60}, {1280, 720, 60}, {1024, 768, 30}, {800, 600, 30}}; -} - -/** - * @brief Get the Audio Modes - */ -immer::array getAudioModes() { - // Stereo - return {{2, 1, 1, {state::AudioMode::FRONT_LEFT, state::AudioMode::FRONT_RIGHT}}}; -} - state::Host get_host_config(std::string_view pkey_filename, std::string_view cert_filename) { X509 *server_cert; EVP_PKEY *server_pkey; @@ -73,7 +58,12 @@ state::Host get_host_config(std::string_view pkey_filename, std::string_view cer mac_address = override_mac; } - return {getDisplayModes(), getAudioModes(), server_cert, server_pkey, internal_ip, mac_address}; + return {state::DISPLAY_CONFIGURATIONS, + state::AUDIO_CONFIGURATIONS, + server_cert, + server_pkey, + internal_ip, + mac_address}; } /** @@ -82,7 +72,6 @@ state::Host get_host_config(std::string_view pkey_filename, std::string_view cer auto initialize(std::string_view config_file, std::string_view pkey_filename, std::string_view cert_filename) { auto event_bus = std::make_shared(); auto config = load_config(config_file, event_bus); - auto display_modes = getDisplayModes(); auto host = get_host_config(pkey_filename, cert_filename); auto state = state::AppState{ @@ -203,10 +192,10 @@ auto setup_sessions_handlers(const immer::box &app_state, auto pulse_sink_name = fmt::format("virtual_sink_{}", session->session_id); std::shared_ptr v_device; if (audio_server && audio_server->server) { - v_device = audio::create_virtual_sink(audio_server->server, - audio::AudioDevice{.sink_name = pulse_sink_name, - .n_channels = session->audio_mode.channels, - .bitrate = 48000}); // TODO: + v_device = audio::create_virtual_sink( + audio_server->server, + audio::AudioDevice{.sink_name = pulse_sink_name, + .mode = state::get_audio_mode(session->audio_channel_count, true)}); } /* Setup devices paths */ diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 6e227c8c..6d102d76 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1,3 +1,5 @@ +cmake_minimum_required(VERSION 3.16...3.24) + # Testing library FetchContent_Declare( Catch2 @@ -96,7 +98,7 @@ target_link_libraries_system(wolftests PRIVATE Catch2::Catch2) ## Test assets -configure_file(assets/config.v2.toml ${CMAKE_CURRENT_BINARY_DIR}/config.v2.toml COPYONLY) +configure_file(assets/config.test.toml ${CMAKE_CURRENT_BINARY_DIR}/config.test.toml COPYONLY) # See: https://github.com/catchorg/Catch2/blob/devel/docs/cmake-integration.md list(APPEND CMAKE_MODULE_PATH ${catch2_SOURCE_DIR}/extras) diff --git a/tests/assets/config.v2.toml b/tests/assets/config.test.toml similarity index 95% rename from tests/assets/config.v2.toml rename to tests/assets/config.test.toml index 3408419d..3a89da7d 100644 --- a/tests/assets/config.v2.toml +++ b/tests/assets/config.test.toml @@ -6,7 +6,7 @@ uuid = "0000-1111-2222-3333" support_hevc = true support_av1 = false # The version of this config file -config_version = 3 +config_version = 4 # A list of paired clients that will be allowed to stream @@ -68,6 +68,8 @@ source = "override DEFAULT SOURCE" default_source = "video_source" default_sink = "video_sink" +defaults = { "coreelements" = { "video_params" = "default" } } + ###################### # AV1 encoders # Order here matters: Wolf will try them in order and pick the first one that's not failing @@ -82,7 +84,7 @@ encoder_pipeline = "" plugin_name = "coreelements" check_elements = ["identity"] video_params = "params" -encoder_pipeline = "h264_pipeline" +encoder_pipeline = "av1_pipeline" ###################### # HEVC encoders @@ -97,7 +99,6 @@ encoder_pipeline = "" [[gstreamer.video.hevc_encoders]] plugin_name = "coreelements" check_elements = ["identity"] -video_params = "params" encoder_pipeline = "hevc_pipeline" @@ -114,7 +115,6 @@ encoder_pipeline = "" [[gstreamer.video.h264_encoders]] plugin_name = "coreelements" check_elements = ["identity"] -video_params = "params" encoder_pipeline = "h264_pipeline" diff --git a/tests/platforms/linux/nvidia.cpp b/tests/platforms/linux/nvidia.cpp index 6c1537de..a074cc51 100644 --- a/tests/platforms/linux/nvidia.cpp +++ b/tests/platforms/linux/nvidia.cpp @@ -1,19 +1,39 @@ #include -#include -#include #include #include +#include +#include +#include +#include #include using Catch::Matchers::Contains; +using Catch::Matchers::ContainsSubstring; using Catch::Matchers::Equals; using Catch::Matchers::SizeIs; +std::string get_nvidia_render_device() { + // iterate over /dev/dri/renderD12* devices + for (const auto &entry : std::filesystem::directory_iterator("/dev/dri")) { + if (entry.path().filename().string().find("renderD12") != std::string::npos) { + auto vendor = get_vendor(entry.path().string()); + logs::log(logs::info, "Found {} with vendor {}", entry.path().string(), (int)vendor); + if (vendor == NVIDIA) { + return entry.path().string(); + } + } + } + return ""; +} + TEST_CASE("libdrm find linked devices", "[NVIDIA]") { - auto devices = linked_devices("/dev/dri/renderD128"); + std::string nvidia_node = get_nvidia_render_device(); + REQUIRE(!nvidia_node.empty()); // If there's no nvidia node, we can't continue + + auto devices = linked_devices(nvidia_node); REQUIRE_THAT(devices, SizeIs(6)); - REQUIRE_THAT(devices, Contains("/dev/dri/card0")); + REQUIRE_THAT(devices, Contains(ContainsSubstring("/dev/dri/card"))); REQUIRE_THAT(devices, Contains("/dev/nvidia0")); REQUIRE_THAT(devices, Contains("/dev/nvidia-modeset")); REQUIRE_THAT(devices, Contains("/dev/nvidia-uvm")); @@ -25,7 +45,10 @@ TEST_CASE("libdrm find linked devices", "[NVIDIA]") { } TEST_CASE("libpci get vendor", "[NVIDIA]") { - REQUIRE(get_vendor("/dev/dri/renderD128") == NVIDIA); + std::string nvidia_node = get_nvidia_render_device(); + REQUIRE(!nvidia_node.empty()); // If there's no nvidia node, we can't continue + + REQUIRE(get_vendor(nvidia_node) == NVIDIA); REQUIRE(get_vendor("/dev/dri/a_non_existing_thing") == UNKNOWN); REQUIRE(get_vendor("software") == UNKNOWN); } \ No newline at end of file diff --git a/tests/testGSTPlugin.cpp b/tests/testGSTPlugin.cpp index ef902c54..1a3d9264 100644 --- a/tests/testGSTPlugin.cpp +++ b/tests/testGSTPlugin.cpp @@ -291,7 +291,7 @@ TEST_CASE_METHOD(GStreamerTestsFixture, "Create RTP VIDEO packets", "[GSTPlugin] auto flatten_packets = gst_buffer_list_unfold(rtp_packets); auto packets_content = gst_buffer_copy_content(flatten_packets); - unsigned char *packets_ptr[total_shards]; + std::vector packets_ptr(total_shards); for (int shard_idx = 0; shard_idx < total_shards; shard_idx++) { packets_ptr[shard_idx] = &packets_content.front() + (shard_idx * rtp_packet_size); } @@ -300,7 +300,7 @@ TEST_CASE_METHOD(GStreamerTestsFixture, "Create RTP VIDEO packets", "[GSTPlugin] std::vector marks = {0, 0, 0, 0}; auto rs = moonlight::fec::create(data_shards, parity_shards); - auto result = moonlight::fec::decode(rs.get(), packets_ptr, &marks.front(), total_shards, rtp_packet_size); + auto result = moonlight::fec::decode(rs.get(), &packets_ptr.front(), &marks.front(), total_shards, rtp_packet_size); REQUIRE(result == 0); REQUIRE_THAT(packets_content, Equals(gst_buffer_copy_content(flatten_packets))); @@ -312,7 +312,7 @@ TEST_CASE_METHOD(GStreamerTestsFixture, "Create RTP VIDEO packets", "[GSTPlugin] std::vector marks = {1, 0, 0, 0}; auto rs = moonlight::fec::create(data_shards, parity_shards); - auto result = moonlight::fec::decode(rs.get(), packets_ptr, &marks.front(), total_shards, rtp_packet_size); + auto result = moonlight::fec::decode(rs.get(), &packets_ptr.front(), &marks.front(), total_shards, rtp_packet_size); REQUIRE(result == 0); diff --git a/tests/testMoonlight.cpp b/tests/testMoonlight.cpp index e48f478e..11a1e1ed 100644 --- a/tests/testMoonlight.cpp +++ b/tests/testMoonlight.cpp @@ -19,7 +19,7 @@ using namespace ranges; TEST_CASE("LocalState load TOML", "[LocalState]") { auto event_bus = std::make_shared(); - auto state = state::load_or_default("config.v2.toml", event_bus); + auto state = state::load_or_default("config.test.toml", event_bus); REQUIRE(state.hostname == "Wolf"); REQUIRE(state.uuid == "0000-1111-2222-3333"); REQUIRE(state.support_hevc); @@ -30,24 +30,25 @@ TEST_CASE("LocalState load TOML", "[LocalState]") { auto first_app = state.apps[0]; REQUIRE_THAT(first_app.base.title, Equals("Firefox")); REQUIRE_THAT(first_app.base.id, Equals("1")); - REQUIRE_THAT(first_app.h264_gst_pipeline, Equals("video_source ! params ! h264_pipeline ! video_sink")); - REQUIRE_THAT(first_app.hevc_gst_pipeline, Equals("video_source ! params ! hevc_pipeline ! video_sink")); + REQUIRE_THAT(first_app.h264_gst_pipeline, Equals("video_source !\ndefault !\nh264_pipeline !\nvideo_sink")); + REQUIRE_THAT(first_app.hevc_gst_pipeline, Equals("video_source !\ndefault !\nhevc_pipeline !\nvideo_sink")); + REQUIRE_THAT(first_app.av1_gst_pipeline, Equals("video_source !\nparams !\nav1_pipeline !\nvideo_sink")); REQUIRE(first_app.joypad_type == moonlight::control::pkts::CONTROLLER_TYPE::AUTO); REQUIRE(first_app.start_virtual_compositor); - REQUIRE(first_app.hevc_encoder == state::UNKNOWN); - REQUIRE(first_app.h264_encoder == state::UNKNOWN); REQUIRE(first_app.render_node == "/dev/dri/renderD128"); REQUIRE_THAT(toml::find(first_app.runner->serialise(), "type").as_string(), Equals("docker")); auto second_app = state.apps[1]; REQUIRE_THAT(second_app.base.title, Equals("Test ball")); REQUIRE_THAT(second_app.base.id, Equals("2")); - REQUIRE_THAT(second_app.h264_gst_pipeline, Equals("override DEFAULT SOURCE ! params ! h264_pipeline ! video_sink")); - REQUIRE_THAT(second_app.hevc_gst_pipeline, Equals("override DEFAULT SOURCE ! params ! hevc_pipeline ! video_sink")); + REQUIRE_THAT(second_app.h264_gst_pipeline, + Equals("override DEFAULT SOURCE !\ndefault !\nh264_pipeline !\nvideo_sink")); + REQUIRE_THAT(second_app.hevc_gst_pipeline, + Equals("override DEFAULT SOURCE !\ndefault !\nhevc_pipeline !\nvideo_sink")); + REQUIRE_THAT(second_app.av1_gst_pipeline, + Equals("override DEFAULT SOURCE !\nparams !\nav1_pipeline !\nvideo_sink")); REQUIRE(!second_app.start_virtual_compositor); REQUIRE(second_app.joypad_type == moonlight::control::pkts::CONTROLLER_TYPE::XBOX); - REQUIRE(second_app.hevc_encoder == state::UNKNOWN); - REQUIRE(second_app.h264_encoder == state::UNKNOWN); REQUIRE(second_app.render_node == "/tmp/dead_beef"); REQUIRE_THAT(toml::find(second_app.runner->serialise(), "type").as_string(), Equals("process")); } @@ -64,7 +65,7 @@ TEST_CASE("LocalState load TOML", "[LocalState]") { TEST_CASE("LocalState pairing information", "[LocalState]") { auto event_bus = std::make_shared(); auto clients_atom = new immer::atom(); - auto cfg = state::Config{.config_source = "config.v2.toml", .paired_clients = *clients_atom}; + auto cfg = state::Config{.config_source = "config.test.toml", .paired_clients = *clients_atom}; auto a_client_cert = "-----BEGIN CERTIFICATE-----\n" "MIICvzCCAaegAwIBAgIBADANBgkqhkiG9w0BAQsFADAjMSEwHwYDVQQDDBhOVklE\n" "SUEgR2FtZVN0cmVhbSBDbGllbnQwHhcNMjEwNzEwMDgzNjE3WhcNNDEwNzA1MDgz\n" @@ -124,7 +125,7 @@ TEST_CASE("LocalState pairing information", "[LocalState]") { TEST_CASE("Mocked serverinfo", "[MoonlightProtocol]") { auto event_bus = std::make_shared(); - auto cfg = state::load_or_default("config.v2.toml", event_bus); + auto cfg = state::load_or_default("config.test.toml", event_bus); immer::array displayModes = {{1920, 1080, 60}, {1024, 768, 30}}; SECTION("server_info conforms with the expected HEVC response") { @@ -186,7 +187,7 @@ TEST_CASE("Mocked serverinfo", "[MoonlightProtocol]") { "3.23.0.74" "0000-1111-2222-3333" "0" - "4097" + "65537" "0" "1" "AA:BB:CC:DD" @@ -328,7 +329,7 @@ TEST_CASE("Pairing moonlight", "[MoonlightProtocol]") { TEST_CASE("applist", "[MoonlightProtocol]") { auto event_bus = std::make_shared(); - auto cfg = state::load_or_default("config.v2.toml", event_bus); + auto cfg = state::load_or_default("config.test.toml", event_bus); auto base_apps = cfg.apps | views::transform([](auto app) { return app.base; }) | to>(); auto result = applist(base_apps); REQUIRE(xml_to_str(result) == "\n" @@ -340,7 +341,7 @@ TEST_CASE("applist", "[MoonlightProtocol]") { TEST_CASE("launch", "[MoonlightProtocol]") { auto event_bus = std::make_shared(); - auto cfg = state::load_or_default("config.v2.toml", event_bus); + auto cfg = state::load_or_default("config.test.toml", event_bus); auto result = launch_success("192.168.1.1", "3021"); REQUIRE(xml_to_str(result) == "\n" "" diff --git a/tests/testRTSP.cpp b/tests/testRTSP.cpp index d6d711cc..d98191d7 100644 --- a/tests/testRTSP.cpp +++ b/tests/testRTSP.cpp @@ -12,6 +12,7 @@ using Catch::Matchers::Equals; using namespace std::string_literals; using namespace state; using namespace rtsp; +using namespace wolf::core::audio; /** * In order to test rtsp::tcp_connection we create a derived class that does the opposite: @@ -252,7 +253,7 @@ TEST_CASE("Custom Parser", "[RTSP]") { state::SessionsAtoms test_init_state() { StreamSession session = { .display_mode = {1920, 1080, 60}, - .audio_mode = {2, 1, 1, {state::AudioMode::FRONT_LEFT, state::AudioMode::FRONT_RIGHT}}, + .audio_channel_count = 2, .app = std::make_shared(state::App{.base = {}, .h264_gst_pipeline = "", .hevc_gst_pipeline = "", @@ -308,9 +309,13 @@ TEST_CASE("Commands", "[RTSP]") { REQUIRE(response); REQUIRE(response.value().response.status_code == 200); REQUIRE(response.value().seq_number == 2); + REQUIRE(response.value().payloads.size() == 5); REQUIRE_THAT(response.value().payloads[0].first, Equals("sprop-parameter-sets")); REQUIRE_THAT(response.value().payloads[0].second, Equals("AAAAAU")); REQUIRE_THAT(response.value().payloads[1].second, Equals("fmtp:97 surround-params=21101")); + REQUIRE_THAT(response.value().payloads[2].second, Equals("fmtp:97 surround-params=642014235")); + REQUIRE_THAT(response.value().payloads[3].second, Equals("fmtp:97 surround-params=85301423675")); + REQUIRE_THAT(response.value().payloads[4].second, Equals("x-ss-general.featureFlags: 3")); }); }