From d756cc559e45682cdb062f3e75c6aaf8edf8c0e2 Mon Sep 17 00:00:00 2001 From: "Alexander \"PapaTutuWawa" Date: Wed, 27 Dec 2023 19:50:50 +0100 Subject: [PATCH] Get PipeWire somewhat running --- build.sh | 11 +- flake.lock | 24 +++ flake.nix | 70 +++++++ include/pipewire.hpp | 1 + src/main.cpp | 4 + src/pipewire.cpp | 457 +++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 562 insertions(+), 5 deletions(-) create mode 100644 flake.lock create mode 100644 flake.nix create mode 100644 include/pipewire.hpp create mode 100644 src/pipewire.cpp diff --git a/build.sh b/build.sh index 5cf9954..a14afa1 100755 --- a/build.sh +++ b/build.sh @@ -6,8 +6,8 @@ cd "$script_dir" CC=${CC:-gcc} CXX=${CXX:-g++} -opts="-O2 -g0 -DNDEBUG -Wall -Wextra -Wshadow" -[ -n "$DEBUG" ] && opts="-O0 -g3 -Wall -Wextra -Wshadow"; +opts="-O2 -g0 -DNDEBUG -Wall -Wextra -Wshadow -g -fpermissive" +[ -n "$DEBUG" ] && opts="-O0 -g3 -Wall -Wextra -Wshadow -fpermissive"; build_wayland_protocol() { wayland-scanner private-code external/wlr-export-dmabuf-unstable-v1.xml external/wlr-export-dmabuf-unstable-v1-protocol.c @@ -25,9 +25,10 @@ build_gsr_kms_server() { } build_gsr() { - dependencies="libavcodec libavformat libavutil x11 xcomposite xrandr libpulse libswresample libavfilter libva libcap libdrm wayland-egl wayland-client" + dependencies="libavcodec libavformat libavutil x11 xcomposite xrandr libpulse libswresample libavfilter libva libcap libdrm wayland-egl wayland-client libpipewire-0.3" includes="$(pkg-config --cflags $dependencies)" - libs="$(pkg-config --libs $dependencies) -ldl -pthread -lm" + libs="$(pkg-config --libs $dependencies) -ldl -pthread -lm -lpipewire-0.3" + $CXX -c src/pipewire.cpp $opts $includes $CC -c src/capture/capture.c $opts $includes $CC -c src/capture/nvfbc.c $opts $includes $CC -c src/capture/xcomposite_cuda.c $opts $includes @@ -48,7 +49,7 @@ build_gsr() { $CXX -c src/sound.cpp $opts $includes $CXX -c src/main.cpp $opts $includes $CXX -o gpu-screen-recorder capture.o nvfbc.o kms_client.o egl.o cuda.o xnvctrl.o overclock.o window_texture.o shader.o \ - color_conversion.o utils.o library_loader.o xcomposite_cuda.o xcomposite_vaapi.o kms_vaapi.o kms_cuda.o wlr-export-dmabuf-unstable-v1-protocol.o sound.o main.o $libs $opts + color_conversion.o utils.o library_loader.o xcomposite_cuda.o xcomposite_vaapi.o kms_vaapi.o kms_cuda.o wlr-export-dmabuf-unstable-v1-protocol.o sound.o pipewire.o main.o $libs $opts } build_wayland_protocol diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..1623a6b --- /dev/null +++ b/flake.lock @@ -0,0 +1,24 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1703013332, + "narHash": "sha256-+tFNwMvlXLbJZXiMHqYq77z/RfmpfpiI3yjL6o/Zo9M=", + "path": "/nix/store/50bgi74d890mpkp90w1jwc5g0dw4dccr-source", + "rev": "54aac082a4d9bb5bbc5c4e899603abfb76a3f6d6", + "type": "path" + }, + "original": { + "id": "nixpkgs", + "type": "indirect" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..6f1c565 --- /dev/null +++ b/flake.nix @@ -0,0 +1,70 @@ +{ + description = "A very basic flake"; + + outputs = { self, nixpkgs }: let + gsr = { stdenv + , lib + , fetchurl + , makeWrapper + , pkg-config + , libXcomposite + , libpulseaudio + , ffmpeg + , wayland + , libdrm + , libva + , libglvnd + , libXrandr + , pipewire + }: + + stdenv.mkDerivation { + pname = "gpu-screen-recorder"; + version = "unstable-2023-11-18"; + + # printf "r%s.%s\n" "$(git rev-list --count HEAD)" "$(git rev-parse --short HEAD)" + src = ./.; + #sourceRoot = "."; + + nativeBuildInputs = [ + pkg-config + makeWrapper + ]; + + buildInputs = [ + libXcomposite + libpulseaudio + ffmpeg + wayland + libdrm + libva + libXrandr + pipewire + ]; + + buildPhase = '' + ./build.sh + ''; + + postInstall = '' + install -Dt $out/bin gpu-screen-recorder gsr-kms-server + mkdir $out/bin/.wrapped + mv $out/bin/gpu-screen-recorder $out/bin/.wrapped/ + makeWrapper "$out/bin/.wrapped/gpu-screen-recorder" "$out/bin/gpu-screen-recorder" \ + --prefix LD_LIBRARY_PATH : ${libglvnd}/lib \ + --prefix PATH : $out/bin + ''; + + meta = with lib; { + description = "A screen recorder that has minimal impact on system performance by recording a window using the GPU only"; + homepage = "https://git.dec05eba.com/gpu-screen-recorder/about/"; + license = licenses.gpl3Only; + maintainers = with maintainers; [ babbaj ]; + platforms = [ "x86_64-linux" ]; + }; + }; + in { + packages.x86_64-linux.gsr = nixpkgs.legacyPackages.x86_64-linux.callPackage gsr {}; + packages.x86_64-linux.default = nixpkgs.legacyPackages.x86_64-linux.callPackage gsr {}; + }; +} diff --git a/include/pipewire.hpp b/include/pipewire.hpp new file mode 100644 index 0000000..8f2d2b1 --- /dev/null +++ b/include/pipewire.hpp @@ -0,0 +1 @@ +void init_pipewire(); \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp index d257bc2..1cf5c34 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -23,6 +23,7 @@ extern "C" { #include #include "../include/sound.hpp" +#include "../include/pipewire.hpp" extern "C" { #include @@ -1374,6 +1375,9 @@ struct Arg { }; int main(int argc, char **argv) { + init_pipewire(); + + return 0; signal(SIGINT, stop_handler); signal(SIGUSR1, save_replay_handler); diff --git a/src/pipewire.cpp b/src/pipewire.cpp new file mode 100644 index 0000000..e0ef894 --- /dev/null +++ b/src/pipewire.cpp @@ -0,0 +1,457 @@ +#include +#include +#include +#include +#include + +#define STR(x) #x +#define AUDIO_CHANNELS 2 + +struct target_client { + const char *app_name; + const char *binary; + uint32_t id; + + struct spa_hook client_listener; +}; + +struct target_port { + uint32_t id; + struct target_node *node; +}; + +struct target_node { + uint32_t client_id; + uint32_t id; + const char *app_name; + + std::vector ports; +}; + +struct sink_port { + uint32_t id; + const char* channel; +}; + +struct data { + struct pw_core *core; + + // The stream we will capture + struct pw_stream *stream; + + // The context to use. + struct pw_context *context; + + // Object to accessing global events. + struct pw_registry *registry; + + // Listener for global events. + struct spa_hook registry_listener; + + // The capture sink. + struct pw_proxy *sink_proxy; + + // Listener for the sink events. + struct spa_hook sink_proxy_listener; + + // The event loop to use. + struct pw_thread_loop *thread_loop; + + // The id of the sink that we created. + uint32_t sink_id; + + // The serial of the sink. + uint32_t sink_serial; + + // Sequence number for forcing a server round trip + int seq; + + std::vector sink_ports; + + std::vector targets; + std::vector nodes; + std::vector ports; + + struct spa_audio_info format; +}; + +static void on_process(void *userdata) +{ + struct data *data = static_cast(userdata); + struct pw_buffer *b; + struct spa_buffer *buf; + + if ((b = pw_stream_dequeue_buffer(data->stream)) == NULL) { + pw_log_warn("out of buffers: %m"); + return; + } + + buf = b->buffer; + if (buf->datas[0].data == NULL) + return; + + printf("got a frame of size %d\n", buf->datas[0].chunk->size); + + pw_stream_queue_buffer(data->stream, b); +} +/* [on_process] */ + +static void on_param_changed(void *userdata, uint32_t id, const struct spa_pod *param) +{ + struct data *data = static_cast(userdata); + + if (param == NULL || id != SPA_PARAM_Format) + return; + + if (spa_format_parse(param, + &data->format.media_type, + &data->format.media_subtype) < 0) + return; + + if (data->format.media_type != SPA_MEDIA_TYPE_audio || + data->format.media_subtype != SPA_MEDIA_SUBTYPE_raw) + return; + + if (spa_format_audio_raw_parse(param, &data->format.info.raw) < 0) + return; + + printf("got audio format:\n"); + printf(" channels: %d\n", data->format.info.raw.channels); + printf(" rate: %d\n", data->format.info.raw.rate); + +} + +void register_target_client(struct data *data, uint32_t id, const char* app_name) { + struct target_client client = {}; + client.binary = NULL; + client.app_name = strdup(app_name); + client.id = id; + + data->targets.push_back(client); +} + +void register_target_node(struct data *data, uint32_t id, uint32_t client_id, const char* app_name) { + struct target_node node = {}; + node.app_name = strdup(app_name); + node.id = id; + node.client_id = client_id; + + data->nodes.push_back(node); +} + +void register_target_port(struct data *data, struct target_node *node, uint32_t id) { + struct target_port port = {}; + port.id = id; + port.node = node; + + data->ports.push_back(port); +} + +static void registry_event_global(void *raw_data, uint32_t id, + uint32_t permissions, const char *type, uint32_t version, + const struct spa_dict *props) +{ + if (!type || !props) + return; + + struct data *data = static_cast(raw_data); + + if (id == data->sink_id) { + const char *serial = spa_dict_lookup(props, PW_KEY_OBJECT_SERIAL); + if (!serial) { + data->sink_serial = 0; + printf("No serial found on capture sink\n"); + } else { + data->sink_serial = strtoul(serial, NULL, 10); + } + } + + if (strcmp(type, PW_TYPE_INTERFACE_Port) == 0) { + const char *nid, *dir, *chn; + if ( + !(nid = spa_dict_lookup(props, PW_KEY_NODE_ID)) || + !(dir = spa_dict_lookup(props, PW_KEY_PORT_DIRECTION)) || + !(chn = spa_dict_lookup(props, PW_KEY_AUDIO_CHANNEL)) + ) { + printf("One or more props not set\n"); + return; + } + + uint32_t node_id = strtoul(nid, NULL, 10); + printf("Port: node id %u\n", node_id); + if (strcmp(dir, "in") == 0 && node_id == data->sink_id && data->sink_id != SPA_ID_INVALID) { + printf("=======\n"); + printf("Found our own sink's port: %d sink_id %d channel %s\n", id, data->sink_id, chn); + printf("=======\n"); + + + data->sink_ports.push_back( + { id, strdup(chn), } + ); + } else if (strcmp(dir, "out") == 0) { + if (data->sink_id == SPA_ID_INVALID) { + printf("Want to process port %d but sink_id is invalid\n", id); + return; + } + struct target_node *n = NULL; + for (auto t : data->nodes) { + if (t.id == node_id) { + n = &t; + break; + } + } + if (!n) { + printf("Target not found\n"); + return; + } + printf("Target found\n"); + + uint32_t p = 0; + for (auto sink_port : data->sink_ports) { + printf("%s = %s\n", sink_port.channel, chn); + if (strcmp(sink_port.channel, chn) == 0) { + p = sink_port.id; + break; + } + } + if (!p) { + printf("Failed to find port for channel %s of port %d\n", chn, id); + return; + } + + struct pw_properties *link_props = pw_properties_new( + PW_KEY_OBJECT_LINGER, "false", + NULL + ); + pw_properties_setf(link_props, PW_KEY_LINK_OUTPUT_NODE, "%u", node_id); + pw_properties_setf(link_props, PW_KEY_LINK_OUTPUT_PORT, "%u", id); + + pw_properties_setf(link_props, PW_KEY_LINK_INPUT_NODE, "%u", data->sink_id); + pw_properties_setf(link_props, PW_KEY_LINK_INPUT_PORT, "%u", p); + + printf( + "Connecting (%d, %d) -> (%d, %d)\n", + node_id, id, + data->sink_id, p + ); + + struct pw_proxy *link_proxy = static_cast( + pw_core_create_object( + data->core, "link-factory", + PW_TYPE_INTERFACE_Link, PW_VERSION_LINK, &link_props->dict, 0 + ) + ); + data->seq = pw_core_sync(data->core, PW_ID_CORE, data->seq); + pw_properties_free(link_props); + + if (!link_proxy) { + printf("!!!!! Failed to connect port %u of node %u to capture sink\n", id, node_id); + return; + } + printf("Connected!\n"); + } + } else if (strcmp(type, PW_TYPE_INTERFACE_Client) == 0) { + const char *client_app_name = spa_dict_lookup(props, PW_KEY_APP_NAME); + printf("Client: app name %s id %d\n", client_app_name, id); + register_target_client( + data, + id, + client_app_name + ); + } else if (strcmp(type, PW_TYPE_INTERFACE_Node) == 0) { + const char *node_name, *media_class; + if (!(node_name = spa_dict_lookup(props, PW_KEY_NODE_NAME)) || + !(media_class = spa_dict_lookup(props, PW_KEY_MEDIA_CLASS))) { + return; + } + + printf("Node: media_class %s node_app %s id %d\n", media_class, node_name, id); + if (strcmp(media_class, "Stream/Output/Audio") == 0) { + const char *node_app_name = spa_dict_lookup(props, PW_KEY_APP_NAME); + if (!node_app_name) { + node_app_name = node_name; + } + + uint32_t client_id = 0; + const char *client_id_str = spa_dict_lookup(props, PW_KEY_CLIENT_ID); + if (client_id_str) { + client_id = strtoul(client_id_str, NULL, 10); + } + + + register_target_node( + data, + id, + client_id, + node_app_name + ); + } + } +} + +static const struct pw_stream_events stream_events = { + PW_VERSION_STREAM_EVENTS, + .param_changed = on_param_changed, + .process = on_process, +}; + +static const struct pw_registry_events registry_events = { + PW_VERSION_REGISTRY_EVENTS, + .global = registry_event_global, +}; + +static void on_sink_proxy_bound(void *userdata, uint32_t global_id) { + struct data *data = static_cast(userdata); + data->sink_id = global_id; + printf("Got id %d\n", global_id); +} + +static void on_sink_proxy_error(void *data, int seq, int res, const char *message) +{ + printf("[pipewire] App capture sink error: seq:%d res:%d :%s", seq, res, message); +} + +static const struct pw_proxy_events sink_proxy_events = { + PW_VERSION_PROXY_EVENTS, + .bound = on_sink_proxy_bound, + .error = on_sink_proxy_error, +}; + +void init_pipewire() { + struct data data = { + 0, + sink_id: SPA_ID_INVALID, + sink_serial: 0, + seq: 0, + sink_ports: std::vector {}, + targets: std::vector {}, + nodes: std::vector {}, + ports: std::vector {}, + }; + const struct spa_pod *params[1]; + uint8_t buffer[2048]; + struct spa_pod_builder b = SPA_POD_BUILDER_INIT(buffer, sizeof(buffer)); + struct pw_properties *props; + pw_init(NULL, NULL); + + data.thread_loop = pw_thread_loop_new("PipeWire thread loop", NULL); + pw_thread_loop_lock(data.thread_loop); + if (pw_thread_loop_start(data.thread_loop) < 0) { + printf("Failed to start thread loop"); + return; + } + + data.context = pw_context_new(pw_thread_loop_get_loop(data.thread_loop), NULL, 0); + data.core = pw_context_connect(data.context, NULL, 0); + pw_core_sync(data.core, PW_ID_CORE, 0); + //pw_thread_loop_wait(data.thread_loop); + pw_thread_loop_unlock(data.thread_loop); + + props = pw_properties_new( + PW_KEY_MEDIA_TYPE, "Audio", + PW_KEY_MEDIA_CATEGORY, "Capture", + PW_KEY_MEDIA_ROLE, "Screen", + PW_KEY_NODE_NAME, "GSR", + PW_KEY_NODE_VIRTUAL, "true", + PW_KEY_AUDIO_CHANNELS, "" STR(AUDIO_CHANNELS) "", + SPA_KEY_AUDIO_POSITION, "FL,FR", + PW_KEY_FACTORY_NAME, "support.null-audio-sink", + PW_KEY_MEDIA_CLASS, "Audio/Sink/Internal", + NULL + ); + data.sink_proxy = static_cast( + pw_core_create_object( + data.core, + "adapter", + PW_TYPE_INTERFACE_Node, PW_VERSION_NODE, &props->dict, 0 + ) + ); + pw_proxy_add_listener( + data.sink_proxy, + &data.sink_proxy_listener, + &sink_proxy_events, + &data + ); + data.registry = pw_core_get_registry(data.core, PW_VERSION_REGISTRY, 0); + printf("Got registry\n"); + spa_zero(data.registry_listener); + pw_registry_add_listener(data.registry, &data.registry_listener, ®istry_events, &data); + printf("Listener registered\n"); + + printf("Waiting for id\n"); + while (data.sink_id == SPA_ID_INVALID || data.sink_serial == 0) { + printf("Poll\n"); + pw_loop_iterate(pw_thread_loop_get_loop(data.thread_loop), -1); + } + printf("Got id\n"); + + enum spa_audio_channel channels[8]; + channels[0] = SPA_AUDIO_CHANNEL_FL; + channels[1] = SPA_AUDIO_CHANNEL_FL; + channels[2] = SPA_AUDIO_CHANNEL_UNKNOWN; + channels[3] = SPA_AUDIO_CHANNEL_UNKNOWN; + channels[4] = SPA_AUDIO_CHANNEL_UNKNOWN; + channels[5] = SPA_AUDIO_CHANNEL_UNKNOWN; + channels[6] = SPA_AUDIO_CHANNEL_UNKNOWN; + channels[7] = SPA_AUDIO_CHANNEL_UNKNOWN; + + params[0] = spa_pod_builder_add_object( + &b, + SPA_TYPE_OBJECT_Format, SPA_PARAM_EnumFormat, + SPA_FORMAT_mediaType, SPA_POD_Id(SPA_MEDIA_TYPE_audio), + SPA_FORMAT_mediaSubtype, SPA_POD_Id(SPA_MEDIA_SUBTYPE_raw), + SPA_FORMAT_AUDIO_channels, SPA_POD_Int(AUDIO_CHANNELS), + SPA_FORMAT_AUDIO_position, SPA_POD_Array(sizeof(enum spa_audio_channel), SPA_TYPE_Id, AUDIO_CHANNELS, channels), + SPA_FORMAT_AUDIO_format, SPA_POD_CHOICE_ENUM_Id( + 8, SPA_AUDIO_FORMAT_U8, SPA_AUDIO_FORMAT_S16_LE, SPA_AUDIO_FORMAT_S32_LE, + SPA_AUDIO_FORMAT_F32_LE, SPA_AUDIO_FORMAT_U8P, SPA_AUDIO_FORMAT_S16P, + SPA_AUDIO_FORMAT_S32P, SPA_AUDIO_FORMAT_F32P + ) + ); + + data.stream = pw_stream_new( + data.core, + "GSR", + pw_properties_new( + PW_KEY_NODE_NAME, "GSR", + PW_KEY_NODE_DESCRIPTION, "GSR Audio Capture", + PW_KEY_MEDIA_TYPE, "Audio", + PW_KEY_MEDIA_CATEGORY, "Capture", + PW_KEY_MEDIA_ROLE, "Production", + PW_KEY_NODE_WANT_DRIVER, "true", + PW_KEY_STREAM_CAPTURE_SINK, "true", + NULL + ) + ); + struct pw_properties *stream_props = pw_properties_new(NULL, NULL); + pw_properties_setf(stream_props, PW_KEY_TARGET_OBJECT, "%u", data.sink_serial); + pw_stream_update_properties(data.stream, &stream_props->dict); + pw_properties_free(stream_props); + + pw_stream_connect( + data.stream, + PW_DIRECTION_INPUT, + PW_ID_ANY, + static_cast(PW_STREAM_FLAG_AUTOCONNECT | PW_STREAM_FLAG_MAP_BUFFERS), + params, + 1 + ); + + struct spa_hook stream_listener; + pw_stream_add_listener( + data.stream, + &stream_listener, + &stream_events, + &data + ); + + while (true) { + pw_loop_iterate(pw_thread_loop_get_loop(data.thread_loop), -1); + } + + pw_proxy_destroy((struct pw_proxy *) data.registry); + pw_proxy_destroy(data.sink_proxy); + pw_stream_destroy(data.stream); + pw_context_destroy(data.context); + pw_thread_loop_destroy(data.thread_loop); +} \ No newline at end of file