aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/cli
diff options
context:
space:
mode:
authorMaria Lisina <sekoohaka.sarisan@gmail.com>2026-08-10 10:38:14 +0500
committerMaria Lisina <sekoohaka.sarisan@gmail.com>2026-08-10 10:38:14 +0500
commit6dc1149d078fc3b04a19696bfb7d5d6940372175 (patch)
tree169f7f71e6a292a4ab61a0493000e1add4430f8e /src/cli
parent6836426ae183b9a7d0ec99c1e147e8ad6edabd92 (diff)
parent80517840f4ae64469721598e8373d81f2e4493dd (diff)
Merge tag '6.0.0' of https://repo.dec05eba.com/gpu-screen-recorder into debian
Diffstat (limited to 'src/cli')
-rw-r--r--src/cli/commands.c366
-rw-r--r--src/cli/ipc.c962
-rw-r--r--src/cli/main.c708
3 files changed, 2036 insertions, 0 deletions
diff --git a/src/cli/commands.c b/src/cli/commands.c
new file mode 100644
index 0000000..9b77f79
--- /dev/null
+++ b/src/cli/commands.c
@@ -0,0 +1,366 @@
+#include "../../include/cli/commands.h"
+#include "../../include/recorder/windowing.h"
+#include "../../include/recorder/codec_select.h"
+#include "../../include/recorder/error.h"
+#include "../../include/capture/v4l2.h"
+#include "../../include/window/window.h"
+#include "../../include/sound.h"
+#include "../../include/utils.h"
+#include "../../include/log.h"
+#ifdef GSR_APP_AUDIO
+#include "../../include/pipewire_audio.h"
+#endif
+#ifdef GSR_PORTAL
+#include "../../include/dbus.h"
+#endif
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <sys/wait.h>
+
+#ifndef GSR_VERSION
+#define GSR_VERSION "unknown"
+#endif
+
+static void list_system_info(bool wayland) {
+ printf("display_server|%s\n", wayland ? "wayland" : "x11");
+ bool supports_app_audio = false;
+#ifdef GSR_APP_AUDIO
+ supports_app_audio = pulseaudio_server_is_pipewire();
+ if(supports_app_audio) {
+ gsr_pipewire_audio audio;
+ if(gsr_pipewire_audio_init(&audio))
+ gsr_pipewire_audio_deinit(&audio);
+ else
+ supports_app_audio = false;
+ }
+#endif
+ printf("supports_app_audio|%s\n", supports_app_audio ? "yes" : "no");
+}
+
+static void list_gpu_info(gsr_egl *egl) {
+ switch(egl->gpu_info.vendor) {
+ case GSR_GPU_VENDOR_AMD:
+ printf("vendor|amd\n");
+ break;
+ case GSR_GPU_VENDOR_INTEL:
+ printf("vendor|intel\n");
+ break;
+ case GSR_GPU_VENDOR_NVIDIA:
+ printf("vendor|nvidia\n");
+ break;
+ case GSR_GPU_VENDOR_BROADCOM:
+ printf("vendor|broadcom\n");
+ break;
+ case GSR_GPU_VENDOR_APPLE:
+ printf("vendor|apple\n");
+ break;
+ }
+ printf("card_path|%s\n", egl->card_path);
+}
+
+static void list_supported_video_codecs(gsr_egl *egl, bool wayland) {
+ // Dont clean it up on purpose to increase shutdown speed
+ gsr_supported_video_codecs supported_video_codecs;
+ get_supported_video_codecs(egl, GSR_VIDEO_CODEC_H264, false, false, &supported_video_codecs);
+
+ gsr_supported_video_codecs supported_video_codecs_vulkan;
+ get_supported_video_codecs(egl, GSR_VIDEO_CODEC_H264_VULKAN, false, false, &supported_video_codecs_vulkan);
+
+ set_supported_video_codecs_ffmpeg(&supported_video_codecs, &supported_video_codecs_vulkan, egl->gpu_info.vendor);
+
+ if(supported_video_codecs.h264.supported)
+ puts("h264");
+ if(avcodec_find_encoder_by_name("libx264"))
+ puts("h264_software");
+ if(supported_video_codecs.hevc.supported)
+ puts("hevc");
+ if(supported_video_codecs.hevc_hdr.supported && wayland)
+ puts("hevc_hdr");
+ if(supported_video_codecs.hevc_10bit.supported)
+ puts("hevc_10bit");
+ if(supported_video_codecs.av1.supported)
+ puts("av1");
+ if(supported_video_codecs.av1_hdr.supported && wayland)
+ puts("av1_hdr");
+ if(supported_video_codecs.av1_10bit.supported)
+ puts("av1_10bit");
+ if(supported_video_codecs.vp8.supported)
+ puts("vp8");
+ if(supported_video_codecs.vp9.supported)
+ puts("vp9");
+ if(supported_video_codecs_vulkan.h264.supported)
+ puts("h264_vulkan");
+ if(supported_video_codecs_vulkan.hevc.supported)
+ puts("hevc_vulkan");
+ if(supported_video_codecs_vulkan.hevc_hdr.supported && wayland)
+ puts("hevc_hdr_vulkan");
+ if(supported_video_codecs_vulkan.hevc_10bit.supported)
+ puts("hevc_10bit_vulkan");
+ if(supported_video_codecs_vulkan.av1.supported)
+ puts("av1_vulkan");
+ if(supported_video_codecs_vulkan.av1_hdr.supported && wayland)
+ puts("av1_hdr_vulkan");
+ if(supported_video_codecs_vulkan.av1_10bit.supported)
+ puts("av1_10bit_vulkan");
+}
+
+void run_recording_saved_script_async(const char *script_file, const char *video_file, const char *type) {
+ char script_file_full[PATH_MAX];
+ script_file_full[0] = '\0';
+ if(!realpath(script_file, script_file_full)) {
+ gsr_log(GSR_LOG_LEVEL_ERROR, "script file not found: %s", script_file);
+ return;
+ }
+
+ const char *args[7];
+ const bool inside_flatpak = getenv("FLATPAK_ID") != NULL;
+
+ if(inside_flatpak) {
+ args[0] = "flatpak-spawn";
+ args[1] = "--host";
+ args[2] = "--";
+ args[3] = script_file_full;
+ args[4] = video_file;
+ args[5] = type;
+ args[6] = NULL;
+ } else {
+ args[0] = script_file_full;
+ args[1] = video_file;
+ args[2] = type;
+ args[3] = NULL;
+ }
+
+ pid_t pid = fork();
+ if(pid == -1) {
+ perror(script_file_full);
+ return;
+ } else if(pid == 0) { // child
+ setsid();
+ signal(SIGHUP, SIG_IGN);
+
+ pid_t second_child = fork();
+ if(second_child == 0) { // child
+ execvp(args[0], (char* const*)args);
+ perror(script_file_full);
+ _exit(127);
+ } else if(second_child != -1) { // parent
+ _exit(0);
+ }
+ } else { // parent
+ waitpid(pid, NULL, 0);
+ }
+}
+
+typedef struct {
+ const gsr_window *window;
+ int num_monitors;
+} capture_options_callback;
+
+static void output_monitor_info(const gsr_monitor *monitor, void *userdata) {
+ capture_options_callback *options = (capture_options_callback*)userdata;
+ if(gsr_window_get_display_server(options->window) == GSR_DISPLAY_SERVER_WAYLAND) {
+ vec2i monitor_size = monitor->size;
+ gsr_monitor_rotation monitor_rotation = GSR_MONITOR_ROT_0;
+ vec2i monitor_position = {0, 0};
+ drm_monitor_get_display_server_data(options->window, monitor, &monitor_rotation, &monitor_position);
+ if(monitor_rotation == GSR_MONITOR_ROT_90 || monitor_rotation == GSR_MONITOR_ROT_270)
+ {
+ const int tmp = monitor_size.x;
+ monitor_size.x = monitor_size.y;
+ monitor_size.y = tmp;
+ }
+ printf("%.*s|%dx%d\n", monitor->name_len, monitor->name, monitor_size.x, monitor_size.y);
+ } else {
+ printf("%.*s|%dx%d\n", monitor->name_len, monitor->name, monitor->size.x, monitor->size.y);
+ }
+ ++options->num_monitors;
+}
+
+static void camera_query_callback(const char *path, const gsr_capture_v4l2_supported_setup *setup, void *userdata) {
+ (void)userdata;
+ printf("%s|%ux%u@%uhz|%s\n", path, setup->resolution.width, setup->resolution.height, gsr_capture_v4l2_framerate_to_number(setup->framerate), gsr_capture_v4l2_pixfmt_to_string(setup->pixfmt));
+}
+
+static int list_monitors(const gsr_window *window, const char *card_path) {
+ capture_options_callback options;
+ options.window = window;
+ options.num_monitors = 0;
+
+ const bool is_x11 = gsr_window_get_display_server(window) == GSR_DISPLAY_SERVER_X11;
+ const gsr_connection_type connection_type = is_x11 ? GSR_CONNECTION_X11 : GSR_CONNECTION_DRM;
+ for_each_active_monitor_output(window, card_path, connection_type, output_monitor_info, &options);
+
+ return options.num_monitors;
+}
+
+static void list_supported_capture_options(const gsr_window *window, const char *card_path, bool do_list_monitors) {
+ const bool wayland = gsr_window_get_display_server(window) == GSR_DISPLAY_SERVER_WAYLAND;
+ if(!wayland) {
+ puts("window");
+ puts("focused");
+ }
+
+ int num_monitors = 0;
+ if(do_list_monitors)
+ num_monitors = list_monitors(window, card_path);
+
+ if(num_monitors > 0)
+ puts("region");
+
+ gsr_capture_v4l2_list_devices(camera_query_callback, NULL);
+
+#ifdef GSR_PORTAL
+ // Desktop portal capture on x11 doesn't seem to be hardware accelerated
+ if(!wayland)
+ return;
+
+ gsr_dbus dbus;
+ if(!gsr_dbus_init(&dbus, NULL))
+ return;
+
+ char *session_handle = NULL;
+ if(gsr_dbus_screencast_create_session(&dbus, &session_handle) == 0)
+ puts("portal");
+
+ gsr_dbus_deinit(&dbus);
+#endif
+}
+
+int version_command(void *userdata) {
+ (void)userdata;
+ puts(GSR_VERSION);
+ fflush(stdout);
+ return 0;
+}
+
+int info_command(void *userdata) {
+ (void)userdata;
+ gsr_windowing windowing;
+ const gsr_windowing_params windowing_params = { .monitor_capture = true };
+ if(gsr_windowing_init(&windowing, &windowing_params) != GSR_ERROR_OK)
+ return 1;
+
+ if(gsr_windowing_load_egl(&windowing, &windowing_params) != GSR_ERROR_OK) {
+ gsr_windowing_deinit(&windowing);
+ return 22;
+ }
+
+ const bool wayland = gsr_windowing_is_wayland(&windowing);
+
+ av_log_set_level(AV_LOG_FATAL);
+
+ puts("section=system_info");
+ list_system_info(wayland);
+ if(windowing.egl.gpu_info.is_steam_deck)
+ puts("is_steam_deck|yes");
+ else
+ puts("is_steam_deck|no");
+ printf("gsr_version|%s\n", GSR_VERSION);
+ puts("section=gpu_info");
+ list_gpu_info(&windowing.egl);
+ puts("section=video_codecs");
+ list_supported_video_codecs(&windowing.egl, wayland);
+ puts("section=image_formats");
+ puts("jpeg");
+ puts("png");
+ puts("section=capture_options");
+ list_supported_capture_options(windowing.window, windowing.egl.card_path, windowing.card_path_found);
+
+ fflush(stdout);
+ gsr_windowing_deinit(&windowing);
+ return 0;
+}
+
+int list_audio_devices_command(void *userdata) {
+ (void)userdata;
+ gsr_audio_devices audio_devices;
+ get_pulseaudio_inputs(&audio_devices);
+
+ if(audio_devices.default_output[0] != '\0')
+ puts("default_output|Default output");
+
+ if(audio_devices.default_input[0] != '\0')
+ puts("default_input|Default input");
+
+ for(size_t i = 0; i < audio_devices.num_items; ++i) {
+ printf("%s|%s\n", audio_devices.items[i].name, audio_devices.items[i].description);
+ }
+
+ gsr_audio_devices_deinit(&audio_devices);
+ fflush(stdout);
+ return 0;
+}
+
+static bool app_audio_query_callback(const char *app_name, void *userdata) {
+ (void)userdata;
+ puts(app_name);
+ return true;
+}
+
+int list_application_audio_command(void *userdata) {
+ (void)userdata;
+#ifdef GSR_APP_AUDIO
+ if(pulseaudio_server_is_pipewire()) {
+ gsr_pipewire_audio audio;
+ if(gsr_pipewire_audio_init(&audio)) {
+ gsr_pipewire_audio_for_each_app(&audio, app_audio_query_callback, NULL);
+ gsr_pipewire_audio_deinit(&audio);
+ }
+ }
+#endif
+
+ fflush(stdout);
+ return 0;
+}
+
+int list_v4l2_devices(void *userdata) {
+ (void)userdata;
+ gsr_capture_v4l2_list_devices(camera_query_callback, NULL);
+
+ fflush(stdout);
+ return 0;
+}
+
+int list_capture_options_command(const char *card_path, void *userdata) {
+ (void)userdata;
+ gsr_windowing windowing;
+ const gsr_windowing_params windowing_params = { .monitor_capture = true };
+ if(gsr_windowing_init(&windowing, &windowing_params) != GSR_ERROR_OK)
+ return 1;
+
+ if(!card_path && gsr_windowing_load_egl(&windowing, &windowing_params) != GSR_ERROR_OK) {
+ gsr_windowing_deinit(&windowing);
+ return 22;
+ }
+
+ if(card_path)
+ list_supported_capture_options(windowing.window, card_path, true);
+ else
+ list_supported_capture_options(windowing.window, windowing.egl.card_path, windowing.card_path_found);
+
+ fflush(stdout);
+ gsr_windowing_deinit(&windowing);
+ return 0;
+}
+
+int list_monitors_command(void *userdata) {
+ (void)userdata;
+ gsr_windowing windowing;
+ const gsr_windowing_params windowing_params = { .monitor_capture = true };
+ if(gsr_windowing_init(&windowing, &windowing_params) != GSR_ERROR_OK)
+ return 1;
+
+ if(gsr_windowing_load_egl(&windowing, &windowing_params) != GSR_ERROR_OK) {
+ gsr_windowing_deinit(&windowing);
+ return 22;
+ }
+
+ if(windowing.card_path_found)
+ list_monitors(windowing.window, windowing.egl.card_path);
+
+ fflush(stdout);
+ gsr_windowing_deinit(&windowing);
+ return 0;
+}
diff --git a/src/cli/ipc.c b/src/cli/ipc.c
new file mode 100644
index 0000000..2fb3000
--- /dev/null
+++ b/src/cli/ipc.c
@@ -0,0 +1,962 @@
+#include "../../include/cli/ipc.h"
+#include "../../include/recorder/error.h"
+#include "../../include/recorder/replay_save.h"
+#include "../../include/json.h"
+#include "../../include/log.h"
+
+#include <stdio.h>
+#include <string.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <inttypes.h>
+#include <limits.h>
+#include <poll.h>
+#include <signal.h>
+#include <unistd.h>
+#include <sys/socket.h>
+#include <sys/stat.h>
+#include <sys/un.h>
+
+#ifdef __linux__
+#include <sys/epoll.h>
+#else
+#include <sys/event.h>
+#include <sys/time.h>
+#endif
+
+#define GSR_IPC_MAX_REQUEST_NAME_SIZE 64
+#define GSR_IPC_MAX_EVENTS (2 + GSR_IPC_MAX_CLIENTS*2)
+#define GSR_IPC_SHUTDOWN_SEND_TIMEOUT_MILLISECONDS 1000
+#define GSR_IPC_SOCKET_MODE 0600
+
+#define GSR_IPC_WAKEUP_QUIT (1 << 0)
+#define GSR_IPC_WAKEUP_COMPLETED_REQUEST (1 << 1)
+
+typedef struct {
+ int64_t id;
+ char name[GSR_IPC_MAX_REQUEST_NAME_SIZE];
+ sj_Value data;
+ bool has_data;
+ const char *request_end;
+} gsr_ipc_request;
+
+typedef struct {
+ int fd;
+ bool readable;
+ bool writable;
+} gsr_ipc_event;
+
+static bool string_is_only_whitespace(const char *str, size_t size) {
+ for(size_t i = 0; i < size; ++i) {
+ if(str[i] != ' ' && str[i] != '\t' && str[i] != '\r' && str[i] != '\n')
+ return false;
+ }
+ return true;
+}
+
+static bool fd_set_cloexec(int fd) {
+ const int flags = fcntl(fd, F_GETFD);
+ return flags != -1 && fcntl(fd, F_SETFD, flags | FD_CLOEXEC) != -1;
+}
+
+static bool fd_set_nonblocking(int fd) {
+ const int flags = fcntl(fd, F_GETFL);
+ return flags != -1 && fcntl(fd, F_SETFL, flags | O_NONBLOCK) != -1;
+}
+
+#ifdef __linux__
+static bool ipc_poller_init(gsr_ipc *self) {
+ self->poll_fd = epoll_create1(EPOLL_CLOEXEC);
+ if(self->poll_fd == -1) {
+ gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_ipc: failed to create an epoll instance, error: %s", strerror(errno));
+ return false;
+ }
+ return true;
+}
+
+static bool ipc_poller_add(gsr_ipc *self, int fd) {
+ struct epoll_event event;
+ memset(&event, 0, sizeof(event));
+ event.events = EPOLLIN | EPOLLET;
+ event.data.fd = fd;
+ return epoll_ctl(self->poll_fd, EPOLL_CTL_ADD, fd, &event) == 0;
+}
+
+static bool ipc_poller_set_write_notify(gsr_ipc *self, int fd, bool enable) {
+ struct epoll_event event;
+ memset(&event, 0, sizeof(event));
+ event.events = EPOLLIN | EPOLLET | (enable ? EPOLLOUT : 0);
+ event.data.fd = fd;
+ return epoll_ctl(self->poll_fd, EPOLL_CTL_MOD, fd, &event) == 0;
+}
+
+/* Returns the number of events, or -1 on failure. Waits until at least one event is available */
+static int ipc_poller_wait(gsr_ipc *self, gsr_ipc_event *events, int events_capacity) {
+ struct epoll_event platform_events[GSR_IPC_MAX_EVENTS];
+ if(events_capacity > GSR_IPC_MAX_EVENTS)
+ events_capacity = GSR_IPC_MAX_EVENTS;
+
+ const int num_events = epoll_wait(self->poll_fd, platform_events, events_capacity, -1);
+ if(num_events == -1)
+ return errno == EINTR ? 0 : -1;
+
+ for(int i = 0; i < num_events; ++i) {
+ events[i].fd = platform_events[i].data.fd;
+ events[i].readable = platform_events[i].events & (EPOLLIN | EPOLLHUP | EPOLLERR);
+ events[i].writable = platform_events[i].events & EPOLLOUT;
+ }
+ return num_events;
+}
+#else
+static bool ipc_poller_init(gsr_ipc *self) {
+ self->poll_fd = kqueue();
+ if(self->poll_fd == -1) {
+ gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_ipc: failed to create a kqueue instance, error: %s", strerror(errno));
+ return false;
+ }
+ fd_set_cloexec(self->poll_fd);
+ return true;
+}
+
+static bool ipc_poller_add(gsr_ipc *self, int fd) {
+ struct kevent change;
+ EV_SET(&change, fd, EVFILT_READ, EV_ADD | EV_CLEAR, 0, 0, NULL);
+ return kevent(self->poll_fd, &change, 1, NULL, 0, NULL) != -1;
+}
+
+static bool ipc_poller_set_write_notify(gsr_ipc *self, int fd, bool enable) {
+ struct kevent change;
+ EV_SET(&change, fd, EVFILT_WRITE, enable ? (EV_ADD | EV_CLEAR) : EV_DELETE, 0, 0, NULL);
+ return kevent(self->poll_fd, &change, 1, NULL, 0, NULL) != -1;
+}
+
+/* Returns the number of events, or -1 on failure. Waits until at least one event is available */
+static int ipc_poller_wait(gsr_ipc *self, gsr_ipc_event *events, int events_capacity) {
+ struct kevent platform_events[GSR_IPC_MAX_EVENTS];
+ if(events_capacity > GSR_IPC_MAX_EVENTS)
+ events_capacity = GSR_IPC_MAX_EVENTS;
+
+ const int num_events = kevent(self->poll_fd, NULL, 0, platform_events, events_capacity, NULL);
+ if(num_events == -1)
+ return errno == EINTR ? 0 : -1;
+
+ for(int i = 0; i < num_events; ++i) {
+ events[i].fd = platform_events[i].ident;
+ events[i].readable = platform_events[i].filter == EVFILT_READ;
+ events[i].writable = platform_events[i].filter == EVFILT_WRITE;
+ }
+ return num_events;
+}
+#endif
+
+/* Only used when the ipc thread exits, to not lose replies that haven't been fully sent yet */
+static bool ipc_send_all_blocking(int fd, const char *data, size_t size) {
+ size_t offset = 0;
+ while(offset < size) {
+ const ssize_t bytes_written = send(fd, data + offset, size - offset, MSG_NOSIGNAL);
+ if(bytes_written > 0) {
+ offset += bytes_written;
+ continue;
+ }
+
+ if(bytes_written == -1 && errno == EINTR)
+ continue;
+
+ if(bytes_written == -1 && (errno == EAGAIN || errno == EWOULDBLOCK)) {
+ struct pollfd poll_fd;
+ poll_fd.fd = fd;
+ poll_fd.events = POLLOUT;
+ poll_fd.revents = 0;
+
+ const int poll_result = poll(&poll_fd, 1, GSR_IPC_SHUTDOWN_SEND_TIMEOUT_MILLISECONDS);
+ if(poll_result == -1 && errno == EINTR)
+ continue;
+
+ if(poll_result <= 0)
+ return false;
+
+ continue;
+ }
+
+ return false;
+ }
+ return true;
+}
+
+static bool ipc_client_send_data(gsr_ipc *self, gsr_ipc_client *client, const char *data, size_t size) {
+ size_t offset = 0;
+ if(client->send_buffer_size == 0) {
+ while(offset < size) {
+ const ssize_t bytes_written = send(client->fd, data + offset, size - offset, MSG_NOSIGNAL);
+ if(bytes_written > 0) {
+ offset += bytes_written;
+ continue;
+ }
+
+ if(bytes_written == -1 && errno == EINTR)
+ continue;
+
+ if(bytes_written == -1 && (errno == EAGAIN || errno == EWOULDBLOCK))
+ break;
+
+ return false;
+ }
+ }
+
+ const size_t bytes_remaining = size - offset;
+ if(bytes_remaining == 0)
+ return true;
+
+ if(client->send_buffer_size + bytes_remaining > sizeof(client->send_buffer)) {
+ gsr_log(GSR_LOG_LEVEL_WARNING, "gsr_ipc: an ipc client isn't reading replies fast enough, disconnecting it");
+ return false;
+ }
+
+ const bool send_buffer_was_empty = client->send_buffer_size == 0;
+ memcpy(client->send_buffer + client->send_buffer_size, data + offset, bytes_remaining);
+ client->send_buffer_size += bytes_remaining;
+ return !send_buffer_was_empty || ipc_poller_set_write_notify(self, client->fd, true);
+}
+
+static bool ipc_client_flush_send_buffer(gsr_ipc *self, gsr_ipc_client *client) {
+ if(client->send_buffer_size == 0)
+ return true;
+
+ size_t offset = 0;
+ while(offset < client->send_buffer_size) {
+ const ssize_t bytes_written = send(client->fd, client->send_buffer + offset, client->send_buffer_size - offset, MSG_NOSIGNAL);
+ if(bytes_written > 0) {
+ offset += bytes_written;
+ continue;
+ }
+
+ if(bytes_written == -1 && errno == EINTR)
+ continue;
+
+ if(bytes_written == -1 && (errno == EAGAIN || errno == EWOULDBLOCK))
+ break;
+
+ return false;
+ }
+
+ memmove(client->send_buffer, client->send_buffer + offset, client->send_buffer_size - offset);
+ client->send_buffer_size -= offset;
+ return client->send_buffer_size != 0 || ipc_poller_set_write_notify(self, client->fd, false);
+}
+
+static bool ipc_client_send_reply(gsr_ipc *self, gsr_ipc_client *client, int64_t id, bool success, const char *error_message, const char *data) {
+ char reply[GSR_IPC_MAX_REPLY_SIZE];
+ int reply_size = 0;
+
+ if(success && data) {
+ char escaped_data[GSR_IPC_MAX_ESCAPED_DATA_SIZE];
+ gsr_json_escape_string(escaped_data, sizeof(escaped_data), data);
+ reply_size = snprintf(reply, sizeof(reply), "{\"id\":%" PRIi64 ",\"result\":\"ok\",\"data\":\"%s\"}\n", id, escaped_data);
+ } else if(success) {
+ reply_size = snprintf(reply, sizeof(reply), "{\"id\":%" PRIi64 ",\"result\":\"ok\"}\n", id);
+ } else {
+ char escaped_error_message[GSR_IPC_MAX_ESCAPED_ERROR_MESSAGE_SIZE];
+ gsr_json_escape_string(escaped_error_message, sizeof(escaped_error_message), error_message ? error_message : "");
+ reply_size = snprintf(reply, sizeof(reply), "{\"id\":%" PRIi64 ",\"result\":\"error\",\"data\":\"%s\"}\n", id, escaped_error_message);
+ }
+
+ if(reply_size < 0 || reply_size >= (int)sizeof(reply)) {
+ gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_ipc: failed to create a reply to request %" PRIi64, id);
+ return false;
+ }
+
+ return ipc_client_send_data(self, client, reply, reply_size);
+}
+
+static bool ipc_request_parse(char *data, size_t size, gsr_ipc_request *request, char *error_message, size_t error_message_size) {
+ memset(request, 0, sizeof(*request));
+ request->request_end = data + size;
+
+ sj_Reader reader = sj_reader(data, size);
+ const sj_Value root = sj_read(&reader);
+ if(root.type != SJ_OBJECT) {
+ snprintf(error_message, error_message_size, "expected the request to be a json object");
+ return false;
+ }
+
+ sj_Value id_value;
+ sj_Value name_value;
+ bool has_id = false;
+ bool has_name = false;
+
+ sj_Value key;
+ sj_Value value;
+ while(sj_iter_object(&reader, root, &key, &value)) {
+ if(gsr_json_string_equals(&key, "id")) {
+ id_value = value;
+ has_id = true;
+ } else if(gsr_json_string_equals(&key, "name")) {
+ name_value = value;
+ has_name = true;
+ } else if(gsr_json_string_equals(&key, "data")) {
+ request->data = value;
+ request->has_data = true;
+ }
+ }
+
+ if(reader.error) {
+ snprintf(error_message, error_message_size, "failed to parse the request: %s", reader.error);
+ return false;
+ }
+
+ if(!has_id) {
+ snprintf(error_message, error_message_size, "the request is missing the 'id' field");
+ return false;
+ }
+
+ if(!gsr_json_number_to_int64(&id_value, &request->id)) {
+ snprintf(error_message, error_message_size, "expected 'id' to be an integer");
+ return false;
+ }
+
+ if(!has_name) {
+ snprintf(error_message, error_message_size, "the request is missing the 'name' field");
+ return false;
+ }
+
+ if(name_value.type != SJ_STRING) {
+ snprintf(error_message, error_message_size, "expected 'name' to be a string");
+ return false;
+ }
+
+ snprintf(request->name, sizeof(request->name), "%.*s", (int)(name_value.end - name_value.start), name_value.start);
+ return true;
+}
+
+static bool json_value_to_save_replay_seconds(const sj_Value *value, int *seconds, char *error_message, size_t error_message_size) {
+ int64_t data_seconds = 0;
+ if(!gsr_json_number_to_int64(value, &data_seconds) || data_seconds <= 0 || data_seconds > INT_MAX) {
+ snprintf(error_message, error_message_size, "expected the number of seconds to save to be larger than 0");
+ return false;
+ }
+
+ *seconds = data_seconds;
+ return true;
+}
+
+static bool ipc_request_get_save_replay_options(const gsr_ipc_request *request, int *seconds, bool *has_restart_replay, bool *restart_replay, char *error_message, size_t error_message_size) {
+ *seconds = GSR_SAVE_REPLAY_SECONDS_FULL;
+ *has_restart_replay = false;
+ *restart_replay = false;
+ if(!request->has_data || request->data.type == SJ_NULL)
+ return true;
+
+ if(request->data.type != SJ_OBJECT) {
+ snprintf(error_message, error_message_size, "expected 'data' to be an object with the optional fields 'seconds' and 'restart-replay'");
+ return false;
+ }
+
+ sj_Reader reader = sj_reader(request->data.start, request->request_end - request->data.start);
+ const sj_Value data = sj_read(&reader);
+
+ sj_Value key;
+ sj_Value value;
+ while(sj_iter_object(&reader, data, &key, &value)) {
+ if(gsr_json_string_equals(&key, "seconds")) {
+ if(value.type == SJ_NULL)
+ continue;
+
+ if(!json_value_to_save_replay_seconds(&value, seconds, error_message, error_message_size))
+ return false;
+ } else if(gsr_json_string_equals(&key, "restart-replay")) {
+ if(value.type != SJ_BOOL) {
+ snprintf(error_message, error_message_size, "expected 'restart-replay' to be true or false");
+ return false;
+ }
+
+ *has_restart_replay = true;
+ *restart_replay = gsr_json_string_equals(&value, "true");
+ }
+ }
+
+ if(reader.error) {
+ snprintf(error_message, error_message_size, "failed to parse 'data': %s", reader.error);
+ return false;
+ }
+
+ return true;
+}
+
+static bool ipc_request_get_set_paused_state(const gsr_ipc_request *request, bool *paused, char *error_message, size_t error_message_size) {
+ if(request->has_data && request->data.type == SJ_BOOL) {
+ *paused = gsr_json_string_equals(&request->data, "true");
+ return true;
+ }
+
+ snprintf(error_message, error_message_size, "expected 'data' to be true to pause or false to unpause");
+ return false;
+}
+
+static bool ipc_request_name_to_deferred_request_type(const char *name, gsr_ipc_deferred_request_type *type) {
+ if(strcmp(name, "stop") == 0) {
+ *type = GSR_IPC_DEFERRED_REQUEST_STOP;
+ return true;
+ }
+
+ if(strcmp(name, "save-replay") == 0) {
+ *type = GSR_IPC_DEFERRED_REQUEST_SAVE_REPLAY;
+ return true;
+ }
+
+ if(strcmp(name, "stop-replay-recording") == 0) {
+ *type = GSR_IPC_DEFERRED_REQUEST_STOP_REPLAY_RECORDING;
+ return true;
+ }
+
+ return false;
+}
+
+static const char* deferred_request_already_pending_error(gsr_ipc_deferred_request_type type) {
+ switch(type) {
+ case GSR_IPC_DEFERRED_REQUEST_STOP: return "GPU Screen Recorder is already stopping";
+ case GSR_IPC_DEFERRED_REQUEST_SAVE_REPLAY: return "a replay is already being saved";
+ case GSR_IPC_DEFERRED_REQUEST_STOP_REPLAY_RECORDING: return "the recording is already being stopped";
+ case GSR_IPC_DEFERRED_REQUEST_TYPE_COUNT: break;
+ }
+ return "the request is already being handled";
+}
+
+static const char* deferred_request_failed_error(gsr_ipc_deferred_request_type type) {
+ switch(type) {
+ case GSR_IPC_DEFERRED_REQUEST_STOP: return "failed to save the recording";
+ case GSR_IPC_DEFERRED_REQUEST_SAVE_REPLAY: return "failed to save the replay";
+ case GSR_IPC_DEFERRED_REQUEST_STOP_REPLAY_RECORDING: return "failed to save the recording";
+ case GSR_IPC_DEFERRED_REQUEST_TYPE_COUNT: break;
+ }
+ return "the request failed";
+}
+
+static bool ipc_set_deferred_request_pending(gsr_ipc *self, gsr_ipc_deferred_request_type type, int client_fd, int64_t request_id) {
+ pthread_mutex_lock(&self->deferred_requests_mutex);
+ gsr_ipc_deferred_request *deferred_request = &self->deferred_requests[type];
+ const bool was_empty = deferred_request->state == GSR_IPC_DEFERRED_REQUEST_STATE_EMPTY;
+ if(was_empty) {
+ deferred_request->state = GSR_IPC_DEFERRED_REQUEST_STATE_PENDING;
+ deferred_request->client_fd = client_fd;
+ deferred_request->request_id = request_id;
+ deferred_request->success = false;
+ deferred_request->has_filepath = false;
+ }
+ pthread_mutex_unlock(&self->deferred_requests_mutex);
+ return was_empty;
+}
+
+static void ipc_clear_deferred_request(gsr_ipc *self, gsr_ipc_deferred_request_type type) {
+ pthread_mutex_lock(&self->deferred_requests_mutex);
+ self->deferred_requests[type].state = GSR_IPC_DEFERRED_REQUEST_STATE_EMPTY;
+ pthread_mutex_unlock(&self->deferred_requests_mutex);
+}
+
+static bool ipc_handle_request(gsr_ipc *self, const gsr_ipc_request *request, char *error_message, size_t error_message_size) {
+ if(strcmp(request->name, "stop") == 0)
+ return self->handlers.stop(error_message, error_message_size, self->handlers.userdata);
+
+ if(strcmp(request->name, "toggle-pause") == 0)
+ return self->handlers.toggle_pause(error_message, error_message_size, self->handlers.userdata);
+
+ if(strcmp(request->name, "set-paused") == 0) {
+ bool paused = false;
+ if(!ipc_request_get_set_paused_state(request, &paused, error_message, error_message_size))
+ return false;
+
+ return self->handlers.set_paused(paused, error_message, error_message_size, self->handlers.userdata);
+ }
+
+ if(strcmp(request->name, "toggle-replay-recording") == 0)
+ return self->handlers.toggle_replay_recording(error_message, error_message_size, self->handlers.userdata);
+
+ if(strcmp(request->name, "start-replay-recording") == 0)
+ return self->handlers.start_replay_recording(error_message, error_message_size, self->handlers.userdata);
+
+ if(strcmp(request->name, "stop-replay-recording") == 0)
+ return self->handlers.stop_replay_recording(error_message, error_message_size, self->handlers.userdata);
+
+ if(strcmp(request->name, "save-replay") == 0) {
+ int seconds = GSR_SAVE_REPLAY_SECONDS_FULL;
+ bool has_restart_replay = false;
+ bool restart_replay = false;
+ if(!ipc_request_get_save_replay_options(request, &seconds, &has_restart_replay, &restart_replay, error_message, error_message_size))
+ return false;
+
+ return self->handlers.save_replay(seconds, has_restart_replay, restart_replay, error_message, error_message_size, self->handlers.userdata);
+ }
+
+ snprintf(error_message, error_message_size, "unknown request name '%s'", request->name);
+ return false;
+}
+
+static bool ipc_client_on_request(gsr_ipc *self, gsr_ipc_client *client) {
+ if(client->request_too_large)
+ return ipc_client_send_reply(self, client, 0, false, "the request is too large", NULL);
+
+ if(string_is_only_whitespace(client->request, client->request_size))
+ return ipc_client_send_reply(self, client, 0, false, "the request is empty", NULL);
+
+ char error_message[GSR_IPC_MAX_ERROR_MESSAGE_SIZE];
+ error_message[0] = '\0';
+
+ gsr_ipc_request request;
+ if(!ipc_request_parse(client->request, client->request_size, &request, error_message, sizeof(error_message)))
+ return ipc_client_send_reply(self, client, request.id, false, error_message, NULL);
+
+ /* The pending deferred request has to be registered before the handler starts the operation,
+ otherwise the operation could finish before the reply to it gets registered */
+ gsr_ipc_deferred_request_type deferred_request_type;
+ const bool reply_is_deferred = ipc_request_name_to_deferred_request_type(request.name, &deferred_request_type);
+ if(reply_is_deferred && !ipc_set_deferred_request_pending(self, deferred_request_type, client->fd, request.id))
+ return ipc_client_send_reply(self, client, request.id, false, deferred_request_already_pending_error(deferred_request_type), NULL);
+
+ if(!ipc_handle_request(self, &request, error_message, sizeof(error_message))) {
+ if(reply_is_deferred)
+ ipc_clear_deferred_request(self, deferred_request_type);
+ return ipc_client_send_reply(self, client, request.id, false, error_message, NULL);
+ }
+
+ if(reply_is_deferred)
+ return true;
+
+ return ipc_client_send_reply(self, client, request.id, true, NULL, NULL);
+}
+
+static bool ipc_client_on_byte(gsr_ipc *self, gsr_ipc_client *client, char c) {
+ if(c != '\n') {
+ if(client->request_size < GSR_IPC_MAX_REQUEST_SIZE)
+ client->request[client->request_size++] = c;
+ else
+ client->request_too_large = true;
+ return true;
+ }
+
+ const bool keep_client = ipc_client_on_request(self, client);
+ client->request_size = 0;
+ client->request_too_large = false;
+ return keep_client;
+}
+
+static bool ipc_client_receive(gsr_ipc *self, gsr_ipc_client *client) {
+ for(;;) {
+ char buffer[1024];
+ const ssize_t bytes_read = recv(client->fd, buffer, sizeof(buffer), 0);
+ if(bytes_read == 0)
+ return false;
+
+ if(bytes_read == -1) {
+ if(errno == EINTR)
+ continue;
+
+ return errno == EAGAIN || errno == EWOULDBLOCK;
+ }
+
+ for(ssize_t i = 0; i < bytes_read; ++i) {
+ if(!ipc_client_on_byte(self, client, buffer[i]))
+ return false;
+ }
+ }
+}
+
+static void ipc_add_client(gsr_ipc *self, int client_fd) {
+ if(self->num_clients == GSR_IPC_MAX_CLIENTS) {
+ gsr_log(GSR_LOG_LEVEL_WARNING, "gsr_ipc: too many ipc clients are connected, rejecting the new connection");
+ close(client_fd);
+ return;
+ }
+
+ if(!fd_set_cloexec(client_fd) || !fd_set_nonblocking(client_fd) || !ipc_poller_add(self, client_fd)) {
+ gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_ipc: failed to setup the ipc client socket, error: %s", strerror(errno));
+ close(client_fd);
+ return;
+ }
+
+ gsr_ipc_client *client = &self->clients[self->num_clients];
+ client->fd = client_fd;
+ client->request_size = 0;
+ client->request_too_large = false;
+ client->send_buffer_size = 0;
+ ++self->num_clients;
+}
+
+static void ipc_accept_clients(gsr_ipc *self) {
+ for(;;) {
+ const int client_fd = accept(self->socket_fd, NULL, NULL);
+ if(client_fd == -1) {
+ if(errno == EINTR)
+ continue;
+ return;
+ }
+
+ ipc_add_client(self, client_fd);
+ }
+}
+
+static void ipc_remove_client(gsr_ipc *self, int index) {
+ const int client_fd = self->clients[index].fd;
+
+ pthread_mutex_lock(&self->deferred_requests_mutex);
+ for(int i = 0; i < GSR_IPC_DEFERRED_REQUEST_TYPE_COUNT; ++i) {
+ if(self->deferred_requests[i].state != GSR_IPC_DEFERRED_REQUEST_STATE_EMPTY && self->deferred_requests[i].client_fd == client_fd)
+ self->deferred_requests[i].state = GSR_IPC_DEFERRED_REQUEST_STATE_EMPTY;
+ }
+ pthread_mutex_unlock(&self->deferred_requests_mutex);
+
+ close(client_fd);
+ for(int i = index; i < self->num_clients - 1; ++i) {
+ self->clients[i] = self->clients[i + 1];
+ }
+ --self->num_clients;
+}
+
+static int ipc_find_client_index_by_fd(const gsr_ipc *self, int fd) {
+ for(int i = 0; i < self->num_clients; ++i) {
+ if(self->clients[i].fd == fd)
+ return i;
+ }
+ return -1;
+}
+
+static void ipc_send_completed_request_replies(gsr_ipc *self) {
+ for(int i = 0; i < GSR_IPC_DEFERRED_REQUEST_TYPE_COUNT; ++i) {
+ pthread_mutex_lock(&self->deferred_requests_mutex);
+ const gsr_ipc_deferred_request deferred_request = self->deferred_requests[i];
+ if(deferred_request.state == GSR_IPC_DEFERRED_REQUEST_STATE_COMPLETED)
+ self->deferred_requests[i].state = GSR_IPC_DEFERRED_REQUEST_STATE_EMPTY;
+ pthread_mutex_unlock(&self->deferred_requests_mutex);
+
+ if(deferred_request.state != GSR_IPC_DEFERRED_REQUEST_STATE_COMPLETED)
+ continue;
+
+ const int client_index = ipc_find_client_index_by_fd(self, deferred_request.client_fd);
+ if(client_index == -1)
+ continue;
+
+ const char *error_message = deferred_request_failed_error(i);
+ const char *filepath = deferred_request.has_filepath ? deferred_request.filepath : NULL;
+ if(!ipc_client_send_reply(self, &self->clients[client_index], deferred_request.request_id, deferred_request.success, error_message, filepath))
+ ipc_remove_client(self, client_index);
+ }
+}
+
+static void ipc_fail_pending_requests(gsr_ipc *self) {
+ for(int i = 0; i < GSR_IPC_DEFERRED_REQUEST_TYPE_COUNT; ++i) {
+ pthread_mutex_lock(&self->deferred_requests_mutex);
+ const gsr_ipc_deferred_request deferred_request = self->deferred_requests[i];
+ self->deferred_requests[i].state = GSR_IPC_DEFERRED_REQUEST_STATE_EMPTY;
+ pthread_mutex_unlock(&self->deferred_requests_mutex);
+
+ if(deferred_request.state != GSR_IPC_DEFERRED_REQUEST_STATE_PENDING)
+ continue;
+
+ const int client_index = ipc_find_client_index_by_fd(self, deferred_request.client_fd);
+ if(client_index == -1)
+ continue;
+
+ if(!ipc_client_send_reply(self, &self->clients[client_index], deferred_request.request_id, false, "GPU Screen Recorder exited before the request finished", NULL))
+ ipc_remove_client(self, client_index);
+ }
+}
+
+static void ipc_flush_clients_blocking(gsr_ipc *self) {
+ for(int i = 0; i < self->num_clients; ++i) {
+ gsr_ipc_client *client = &self->clients[i];
+ if(client->send_buffer_size > 0)
+ ipc_send_all_blocking(client->fd, client->send_buffer, client->send_buffer_size);
+ client->send_buffer_size = 0;
+ }
+}
+
+static int ipc_drain_wakeup_pipe(gsr_ipc *self) {
+ int wakeup_flags = 0;
+ for(;;) {
+ char buffer[64];
+ const ssize_t bytes_read = read(self->wakeup_pipe[0], buffer, sizeof(buffer));
+ if(bytes_read == -1 && errno == EINTR)
+ continue;
+
+ if(bytes_read <= 0)
+ break;
+
+ for(ssize_t i = 0; i < bytes_read; ++i) {
+ if(buffer[i] == 'q')
+ wakeup_flags |= GSR_IPC_WAKEUP_QUIT;
+ else if(buffer[i] == 'c')
+ wakeup_flags |= GSR_IPC_WAKEUP_COMPLETED_REQUEST;
+ }
+ }
+ return wakeup_flags;
+}
+
+static void ipc_wakeup_thread(gsr_ipc *self, char wakeup_value) {
+ ssize_t bytes_written = 0;
+ do {
+ bytes_written = write(self->wakeup_pipe[1], &wakeup_value, 1);
+ } while(bytes_written == -1 && errno == EINTR);
+
+ if(bytes_written == -1)
+ gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_ipc: failed to wake up the ipc thread, error: %s", strerror(errno));
+}
+
+static void* ipc_thread(void *userdata) {
+ gsr_ipc *self = userdata;
+ gsr_ipc_event events[GSR_IPC_MAX_EVENTS];
+ bool running = true;
+
+ while(running) {
+ const int num_events = ipc_poller_wait(self, events, GSR_IPC_MAX_EVENTS);
+ if(num_events == -1) {
+ gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_ipc: failed to wait for ipc events, error: %s", strerror(errno));
+ break;
+ }
+
+ for(int i = 0; i < num_events; ++i) {
+ if(events[i].fd == self->wakeup_pipe[0]) {
+ const int wakeup_flags = ipc_drain_wakeup_pipe(self);
+ if(wakeup_flags & GSR_IPC_WAKEUP_COMPLETED_REQUEST)
+ ipc_send_completed_request_replies(self);
+ if(wakeup_flags & GSR_IPC_WAKEUP_QUIT)
+ running = false;
+ continue;
+ }
+
+ if(events[i].fd == self->socket_fd) {
+ ipc_accept_clients(self);
+ continue;
+ }
+
+ const int client_index = ipc_find_client_index_by_fd(self, events[i].fd);
+ if(client_index == -1)
+ continue;
+
+ gsr_ipc_client *client = &self->clients[client_index];
+ bool keep_client = true;
+ if(events[i].readable)
+ keep_client = ipc_client_receive(self, client);
+ if(keep_client && events[i].writable)
+ keep_client = ipc_client_flush_send_buffer(self, client);
+
+ if(!keep_client)
+ ipc_remove_client(self, client_index);
+ }
+ }
+
+ ipc_send_completed_request_replies(self);
+ ipc_fail_pending_requests(self);
+ ipc_flush_clients_blocking(self);
+ return NULL;
+}
+
+static bool ipc_socket_filepath_in_use(const char *socket_filepath) {
+ struct sockaddr_un addr;
+ memset(&addr, 0, sizeof(addr));
+ addr.sun_family = AF_UNIX;
+ snprintf(addr.sun_path, sizeof(addr.sun_path), "%s", socket_filepath);
+
+ const int fd = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0);
+ if(fd == -1)
+ return true;
+
+ const bool in_use = connect(fd, (const struct sockaddr*)&addr, sizeof(addr)) == 0;
+ close(fd);
+ return in_use;
+}
+
+/* Removes the socket that a GPU Screen Recorder instance that was killed left behind, so the same ipc socket path can be used again */
+static bool ipc_remove_unused_socket(const char *socket_filepath) {
+ struct stat file_stat;
+ if(lstat(socket_filepath, &file_stat) == -1)
+ return true;
+
+ if(!S_ISSOCK(file_stat.st_mode)) {
+ gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_ipc_init: can't use \"%s\" as the ipc socket path because a file that isn't a socket already exists there", socket_filepath);
+ return false;
+ }
+
+ if(ipc_socket_filepath_in_use(socket_filepath)) {
+ gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_ipc_init: another program is already listening on \"%s\"", socket_filepath);
+ return false;
+ }
+
+ unlink(socket_filepath);
+ return true;
+}
+
+static bool ipc_bind(gsr_ipc *self, const struct sockaddr_un *addr) {
+ if(!ipc_remove_unused_socket(self->socket_filepath))
+ return false;
+
+ const mode_t prev_mask = umask(0777 & ~GSR_IPC_SOCKET_MODE);
+ const int bind_result = bind(self->socket_fd, (const struct sockaddr*)addr, sizeof(*addr));
+ const int bind_error = errno;
+ umask(prev_mask);
+
+ if(bind_result == -1) {
+ gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_ipc_init: failed to bind the ipc socket to \"%s\", error: %s", self->socket_filepath, strerror(bind_error));
+ return false;
+ }
+
+ self->socket_bound = true;
+ return true;
+}
+
+static void ipc_close(gsr_ipc *self) {
+ for(int i = 0; i < self->num_clients; ++i) {
+ close(self->clients[i].fd);
+ }
+ self->num_clients = 0;
+
+ for(int i = 0; i < 2; ++i) {
+ if(self->wakeup_pipe[i] != -1) {
+ close(self->wakeup_pipe[i]);
+ self->wakeup_pipe[i] = -1;
+ }
+ }
+
+ if(self->poll_fd != -1) {
+ close(self->poll_fd);
+ self->poll_fd = -1;
+ }
+
+ if(self->socket_fd != -1) {
+ close(self->socket_fd);
+ self->socket_fd = -1;
+ }
+
+ if(self->socket_bound) {
+ unlink(self->socket_filepath);
+ self->socket_bound = false;
+ }
+
+ if(self->deferred_requests_mutex_created) {
+ pthread_mutex_destroy(&self->deferred_requests_mutex);
+ self->deferred_requests_mutex_created = false;
+ }
+}
+
+int gsr_ipc_init(gsr_ipc *self, const char *socket_filepath) {
+ memset(self, 0, sizeof(*self));
+ self->socket_fd = -1;
+ self->poll_fd = -1;
+ self->wakeup_pipe[0] = -1;
+ self->wakeup_pipe[1] = -1;
+
+ struct sockaddr_un addr;
+ memset(&addr, 0, sizeof(addr));
+ addr.sun_family = AF_UNIX;
+ if(snprintf(addr.sun_path, sizeof(addr.sun_path), "%s", socket_filepath) >= (int)sizeof(addr.sun_path)) {
+ gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_ipc_init: the ipc socket path is too long, it can be at most %d characters: \"%s\"", (int)sizeof(addr.sun_path) - 1, socket_filepath);
+ goto err;
+ }
+
+ snprintf(self->socket_filepath, sizeof(self->socket_filepath), "%s", socket_filepath);
+
+ if(pthread_mutex_init(&self->deferred_requests_mutex, NULL) != 0) {
+ gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_ipc_init: failed to create the deferred requests mutex");
+ goto err;
+ }
+ self->deferred_requests_mutex_created = true;
+
+ if(pipe(self->wakeup_pipe) == -1) {
+ gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_ipc_init: failed to create the ipc wakeup pipe, error: %s", strerror(errno));
+ self->wakeup_pipe[0] = -1;
+ self->wakeup_pipe[1] = -1;
+ goto err;
+ }
+
+ if(!fd_set_cloexec(self->wakeup_pipe[0]) || !fd_set_cloexec(self->wakeup_pipe[1]) || !fd_set_nonblocking(self->wakeup_pipe[0])) {
+ gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_ipc_init: failed to setup the ipc wakeup pipe, error: %s", strerror(errno));
+ goto err;
+ }
+
+ self->socket_fd = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC | SOCK_NONBLOCK, 0);
+ if(self->socket_fd == -1) {
+ gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_ipc_init: failed to create the ipc socket, error: %s", strerror(errno));
+ goto err;
+ }
+
+ if(!ipc_poller_init(self))
+ goto err;
+
+ if(!ipc_poller_add(self, self->wakeup_pipe[0]) || !ipc_poller_add(self, self->socket_fd)) {
+ gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_ipc_init: failed to register the ipc sockets for events, error: %s", strerror(errno));
+ goto err;
+ }
+
+ if(!ipc_bind(self, &addr))
+ goto err;
+
+ if(listen(self->socket_fd, GSR_IPC_MAX_CLIENTS) == -1) {
+ gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_ipc_init: failed to listen on the ipc socket, error: %s", strerror(errno));
+ goto err;
+ }
+
+ self->initialized = true;
+ return GSR_ERROR_OK;
+
+ err:
+ ipc_close(self);
+ return GSR_ERROR_GENERIC;
+}
+
+void gsr_ipc_deinit(gsr_ipc *self) {
+ if(!self->initialized)
+ return;
+
+ gsr_ipc_stop(self);
+ ipc_close(self);
+ self->initialized = false;
+}
+
+int gsr_ipc_start(gsr_ipc *self, const gsr_ipc_handlers *handlers) {
+ if(!self->initialized)
+ return GSR_ERROR_OK;
+
+ self->handlers = *handlers;
+
+ /* Block all signals in the ipc thread to keep the signal handlers running on the main thread */
+ sigset_t all_signals;
+ sigset_t prev_signals;
+ sigfillset(&all_signals);
+ pthread_sigmask(SIG_SETMASK, &all_signals, &prev_signals);
+ const int thread_create_result = pthread_create(&self->thread, NULL, ipc_thread, self);
+ pthread_sigmask(SIG_SETMASK, &prev_signals, NULL);
+
+ if(thread_create_result != 0) {
+ gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_ipc_start: failed to create the ipc thread, error: %s", strerror(thread_create_result));
+ return GSR_ERROR_GENERIC;
+ }
+
+ self->thread_running = true;
+ return GSR_ERROR_OK;
+}
+
+void gsr_ipc_stop(gsr_ipc *self) {
+ if(!self->thread_running)
+ return;
+
+ ipc_wakeup_thread(self, 'q');
+ pthread_join(self->thread, NULL);
+ self->thread_running = false;
+}
+
+void gsr_ipc_complete_request(gsr_ipc *self, gsr_ipc_deferred_request_type type, bool success, const char *filepath) {
+ if(!self->initialized)
+ return;
+
+ pthread_mutex_lock(&self->deferred_requests_mutex);
+ gsr_ipc_deferred_request *deferred_request = &self->deferred_requests[type];
+ const bool was_pending = deferred_request->state == GSR_IPC_DEFERRED_REQUEST_STATE_PENDING;
+ if(was_pending) {
+ deferred_request->state = GSR_IPC_DEFERRED_REQUEST_STATE_COMPLETED;
+ deferred_request->success = success;
+ deferred_request->has_filepath = filepath != NULL;
+ if(filepath)
+ snprintf(deferred_request->filepath, sizeof(deferred_request->filepath), "%s", filepath);
+ }
+ pthread_mutex_unlock(&self->deferred_requests_mutex);
+
+ if(was_pending)
+ ipc_wakeup_thread(self, 'c');
+}
diff --git a/src/cli/main.c b/src/cli/main.c
new file mode 100644
index 0000000..aaa315c
--- /dev/null
+++ b/src/cli/main.c
@@ -0,0 +1,708 @@
+#include "../../include/cli/commands.h"
+#include "../../include/cli/ipc.h"
+#include "../../include/recorder/recorder.h"
+#include "../../include/recorder/screenshot.h"
+#include "../../include/recorder/capture_source.h"
+#include "../../include/recorder/capture_setup.h"
+#include "../../include/recorder/windowing.h"
+#include "../../include/recorder/audio_input.h"
+#include "../../include/recorder/replay_save.h"
+#include "../../include/recorder/error.h"
+#include "../../include/args_parser.h"
+#include "../../include/sound.h"
+#include "../../include/shader.h"
+#include "../../include/utils.h"
+#include "../../include/log.h"
+#ifdef GSR_APP_AUDIO
+#include "../../include/pipewire_audio.h"
+#endif
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <assert.h>
+#include <locale.h>
+#include <signal.h>
+#include <stdatomic.h>
+#include <unistd.h>
+#include <malloc.h>
+
+static atomic_int running = 1;
+static gsr_recorder *recorder = NULL;
+/* Signals that are received before the recorder has been created are applied when it has been created */
+static volatile sig_atomic_t pending_toggle_pause = 0;
+static volatile sig_atomic_t pending_toggle_replay_recording = 0;
+static volatile sig_atomic_t pending_save_replay_seconds = 0;
+
+static void stop_handler(int signal_value) {
+ (void)signal_value;
+ atomic_store(&running, 0);
+ if(recorder)
+ gsr_recorder_stop(recorder);
+}
+
+static void toggle_pause_handler(int signal_value) {
+ (void)signal_value;
+ if(recorder)
+ gsr_recorder_toggle_pause(recorder);
+ else
+ pending_toggle_pause = 1;
+}
+
+static void toggle_replay_recording_handler(int signal_value) {
+ (void)signal_value;
+ if(recorder)
+ gsr_recorder_toggle_replay_recording(recorder);
+ else
+ pending_toggle_replay_recording = 1;
+}
+
+static void save_replay_seconds_handler(gsr_recorder *rec, int seconds) {
+ if(rec)
+ gsr_recorder_save_replay(rec, seconds, GSR_RESTART_REPLAY_USE_OPTION);
+ else
+ pending_save_replay_seconds = seconds;
+}
+
+static void apply_pending_signals(gsr_recorder *rec) {
+ if(pending_toggle_pause) {
+ pending_toggle_pause = 0;
+ gsr_recorder_toggle_pause(rec);
+ }
+
+ if(pending_toggle_replay_recording) {
+ pending_toggle_replay_recording = 0;
+ gsr_recorder_toggle_replay_recording(rec);
+ }
+
+ if(pending_save_replay_seconds != 0) {
+ const int seconds = pending_save_replay_seconds;
+ pending_save_replay_seconds = 0;
+ gsr_recorder_save_replay(rec, seconds, GSR_RESTART_REPLAY_USE_OPTION);
+ }
+}
+
+static void save_replay_handler(int signal_value) {
+ (void)signal_value;
+ save_replay_seconds_handler(recorder, GSR_SAVE_REPLAY_SECONDS_FULL);
+}
+
+static void save_replay_10_seconds_handler(int signal_value) {
+ (void)signal_value;
+ save_replay_seconds_handler(recorder, 10);
+}
+
+static void save_replay_30_seconds_handler(int signal_value) {
+ (void)signal_value;
+ save_replay_seconds_handler(recorder, 30);
+}
+
+static void save_replay_1_minute_handler(int signal_value) {
+ (void)signal_value;
+ save_replay_seconds_handler(recorder, 60);
+}
+
+static void save_replay_5_minutes_handler(int signal_value) {
+ (void)signal_value;
+ save_replay_seconds_handler(recorder, 60*5);
+}
+
+static void save_replay_10_minutes_handler(int signal_value) {
+ (void)signal_value;
+ save_replay_seconds_handler(recorder, 60*10);
+}
+
+static void save_replay_30_minutes_handler(int signal_value) {
+ (void)signal_value;
+ save_replay_seconds_handler(recorder, 60*30);
+}
+
+static bool ipc_stop_handler(char *error_message, size_t error_message_size, void *userdata) {
+ (void)error_message;
+ (void)error_message_size;
+ (void)userdata;
+ atomic_store(&running, 0);
+ gsr_recorder_stop(recorder);
+ return true;
+}
+
+static bool ipc_toggle_pause_handler(char *error_message, size_t error_message_size, void *userdata) {
+ const gsr_recorder_settings *settings = userdata;
+ if(settings->is_replaying) {
+ snprintf(error_message, error_message_size, "pausing is not supported when recording a replay");
+ return false;
+ }
+
+ gsr_recorder_toggle_pause(recorder);
+ return true;
+}
+
+static bool ipc_set_paused_handler(bool paused, char *error_message, size_t error_message_size, void *userdata) {
+ const gsr_recorder_settings *settings = userdata;
+ if(settings->is_replaying) {
+ snprintf(error_message, error_message_size, "pausing is not supported when recording a replay");
+ return false;
+ }
+
+ gsr_recorder_set_paused(recorder, paused);
+ return true;
+}
+
+static bool ipc_toggle_replay_recording_handler(char *error_message, size_t error_message_size, void *userdata) {
+ const gsr_recorder_settings *settings = userdata;
+ if(!settings->replay_recording_directory) {
+ snprintf(error_message, error_message_size, "option -ro is required to start a recording");
+ return false;
+ }
+
+ gsr_recorder_toggle_replay_recording(recorder);
+ return true;
+}
+
+static bool ipc_start_replay_recording_handler(char *error_message, size_t error_message_size, void *userdata) {
+ const gsr_recorder_settings *settings = userdata;
+ if(!settings->replay_recording_directory) {
+ snprintf(error_message, error_message_size, "option -ro is required to start a recording");
+ return false;
+ }
+
+ gsr_recorder_start_replay_recording(recorder);
+ return true;
+}
+
+static bool ipc_stop_replay_recording_handler(char *error_message, size_t error_message_size, void *userdata) {
+ const gsr_recorder_settings *settings = userdata;
+ if(!settings->replay_recording_directory) {
+ snprintf(error_message, error_message_size, "option -ro is required to start a recording");
+ return false;
+ }
+
+ if(!gsr_recorder_is_replay_recording(recorder)) {
+ snprintf(error_message, error_message_size, "no recording is running");
+ return false;
+ }
+
+ gsr_recorder_stop_replay_recording(recorder);
+ return true;
+}
+
+static bool ipc_save_replay_handler(int seconds, bool has_restart_replay, bool restart_replay, char *error_message, size_t error_message_size, void *userdata) {
+ const gsr_recorder_settings *settings = userdata;
+ if(!settings->is_replaying) {
+ snprintf(error_message, error_message_size, "option -r is required to save a replay");
+ return false;
+ }
+
+ int restart_replay_request = GSR_RESTART_REPLAY_USE_OPTION;
+ if(has_restart_replay)
+ restart_replay_request = restart_replay ? GSR_RESTART_REPLAY_ENABLE : GSR_RESTART_REPLAY_DISABLE;
+
+ gsr_recorder_save_replay(recorder, seconds, restart_replay_request);
+ return true;
+}
+
+static void install_signal_handlers(void) {
+ signal(SIGINT, stop_handler);
+ signal(SIGTERM, stop_handler);
+ signal(SIGUSR1, save_replay_handler);
+ signal(SIGUSR2, toggle_pause_handler);
+ signal(SIGRTMIN, toggle_replay_recording_handler);
+ signal(SIGRTMIN+1, save_replay_10_seconds_handler);
+ signal(SIGRTMIN+2, save_replay_30_seconds_handler);
+ signal(SIGRTMIN+3, save_replay_1_minute_handler);
+ signal(SIGRTMIN+4, save_replay_5_minutes_handler);
+ signal(SIGRTMIN+5, save_replay_10_minutes_handler);
+ signal(SIGRTMIN+6, save_replay_30_minutes_handler);
+}
+
+static void set_display_server_environment_variables(void) {
+ /* Some users dont have properly setup environments (no display manager that does systemctl --user import-environment DISPLAY WAYLAND_DISPLAY) */
+ const char *display = getenv("DISPLAY");
+ if(!display) {
+ display = ":0";
+ setenv("DISPLAY", display, true);
+ }
+
+ const char *wayland_display = getenv("WAYLAND_DISPLAY");
+ if(!wayland_display) {
+ wayland_display = "wayland-0";
+ setenv("WAYLAND_DISPLAY", wayland_display, true);
+ }
+}
+
+static void set_environment_variables(void) {
+ set_display_server_environment_variables();
+
+ /* Linux nvidia driver 580.105.08 added the environment variable CUDA_DISABLE_PERF_BOOST to disable the p2 power level issue,
+ where running cuda (which includes nvenc) causes the gpu to be forcefully set to p2 power level which on many nvidia gpus
+ decreases gpu performance in games. On my GTX 1080 it decreased game performance by 10% for absolutely no reason. */
+ setenv("CUDA_DISABLE_PERF_BOOST", "1", true);
+ /* Stop nvidia driver from buffering frames */
+ setenv("__GL_MaxFramesAllowed", "1", true);
+ /* If this is set to 1 then cuGraphicsGLRegisterImage will fail for egl context with error: invalid OpenGL or DirectX context,
+ so we overwrite it */
+ setenv("__GL_THREADED_OPTIMIZATIONS", "0", true);
+ /* Some people set this to nvidia (for nvdec) or vdpau (for nvidia vdpau), which breaks gpu screen recorder since
+ nvidia doesn't support vaapi and nvidia-vaapi-driver doesn't support encoding yet.
+ Let vaapi find the right vaapi driver instead of forcing a specific one. */
+ unsetenv("LIBVA_DRIVER_NAME");
+ /* Some people set this to force all applications to vsync on nvidia, but this makes eglSwapBuffers never return. */
+ unsetenv("__GL_SYNC_TO_VBLANK");
+ /* Same as above, but for amd/intel */
+ unsetenv("vblank_mode");
+}
+
+static void install_cuda_no_stable_perf_limit(void) {
+ if(access("/proc/driver/nvidia/version", F_OK) != 0)
+ return;
+
+ const char *home = getenv("HOME");
+ if(!home) {
+ gsr_log(GSR_LOG_LEVEL_WARNING, "install_cuda_no_stable_perf_limit: $HOME not set");
+ return;
+ }
+
+ char nv_profiles_path[4096];
+ snprintf(nv_profiles_path, sizeof(nv_profiles_path), "%s/.nv/nvidia-application-profiles-rc.d", home);
+
+ if(create_directory_recursive(nv_profiles_path) != 0) {
+ gsr_log(GSR_LOG_LEVEL_WARNING, "install_cuda_no_stable_perf_limit: failed to create directory: %s", nv_profiles_path);
+ return;
+ }
+
+ snprintf(nv_profiles_path, sizeof(nv_profiles_path), "%s/.nv/nvidia-application-profiles-rc.d/10-gsr-cuda-no-stable-perf-limit", home);
+
+ FILE *f = fopen(nv_profiles_path, "wb");
+ if(!f) {
+ gsr_log(GSR_LOG_LEVEL_WARNING, "install_cuda_no_stable_perf_limit: failed to create file: %s", nv_profiles_path);
+ return;
+ }
+
+ const char *profile_data =
+ "{\n"
+ " \"profiles\": [\n"
+ " {\n"
+ " \"name\": \"CudaNoStablePerfLimit\",\n"
+ " \"settings\": [\"0x166c5e\", 0]\n"
+ " }\n"
+ " ],\n"
+ " \"rules\": [\n"
+ " { \"pattern\": \"gpu-screen-recorder\", \"profile\": \"CudaNoStablePerfLimit\" }\n"
+ " ]\n"
+ "}\n";
+
+ fwrite(profile_data, 1, strlen(profile_data), f);
+ fclose(f);
+}
+
+static int validate_args_with_capture_sources(args_parser *arg_parser, const gsr_capture_sources *capture_sources) {
+ const Arg *output_resolution_arg = args_parser_get_arg(arg_parser, "-s");
+ assert(output_resolution_arg);
+
+ const Arg *region_arg = args_parser_get_arg(arg_parser, "-region");
+ assert(region_arg);
+
+ if(gsr_capture_sources_has_type(capture_sources, GSR_CAPTURE_SOURCE_TYPE_FOCUSED_WINDOW) && output_resolution_arg->num_values == 0) {
+ gsr_log(GSR_LOG_LEVEL_ERROR, "option -s is required when using '-w focused' option");
+ args_parser_print_usage();
+ return GSR_ERROR_GENERIC;
+ }
+
+ const bool is_capturing_region = gsr_capture_sources_has_type(capture_sources, GSR_CAPTURE_SOURCE_TYPE_REGION);
+ if(region_arg->num_values == 0) {
+ if(is_capturing_region && !gsr_capture_sources_has_region_set(capture_sources)) {
+ gsr_log(GSR_LOG_LEVEL_ERROR, "option -region is required when '-w region' is used");
+ args_parser_print_usage();
+ return GSR_ERROR_GENERIC;
+ }
+ } else {
+ if(is_capturing_region) {
+ gsr_log(GSR_LOG_LEVEL_WARNING, "option -region is deprecated, use -w with region directly instead, for example: -w %s", region_arg->values[0]);
+ } else {
+ gsr_log(GSR_LOG_LEVEL_ERROR, "option -region can only be used when option '-w region' is used");
+ args_parser_print_usage();
+ return GSR_ERROR_GENERIC;
+ }
+ }
+
+ if(!arg_parser->settings.restore_portal_session && gsr_capture_sources_has_type(capture_sources, GSR_CAPTURE_SOURCE_TYPE_PORTAL))
+ gsr_log(GSR_LOG_LEVEL_INFO, "option '-w portal' was used without '-restore-portal-session yes'. The previous screencast session will be ignored");
+
+ return GSR_ERROR_OK;
+}
+
+static void screenshot_saved_callback(const char *filepath, void *userdata) {
+ const char *recording_saved_script = userdata;
+ if(recording_saved_script)
+ run_recording_saved_script_async(recording_saved_script, filepath, "screenshot");
+}
+
+typedef struct {
+ const char *recording_saved_script;
+ gsr_ipc *ipc;
+} recorder_callbacks_context;
+
+static void replay_saved_callback(const char *filepath, void *userdata) {
+ recorder_callbacks_context *context = userdata;
+ if(!filepath) {
+ printf("gsr error: Failed to save replay\n");
+ fflush(stdout);
+ gsr_ipc_complete_request(context->ipc, GSR_IPC_DEFERRED_REQUEST_SAVE_REPLAY, false, NULL);
+ return;
+ }
+
+ puts(filepath);
+ fflush(stdout);
+ if(context->recording_saved_script)
+ run_recording_saved_script_async(context->recording_saved_script, filepath, "replay");
+
+ gsr_ipc_complete_request(context->ipc, GSR_IPC_DEFERRED_REQUEST_SAVE_REPLAY, true, filepath);
+}
+
+static void recording_started_callback(const char *filepath, void *userdata) {
+ (void)userdata;
+ if(!filepath) {
+ printf("gsr error: Failed to start recording\n");
+ fflush(stdout);
+ }
+}
+
+static void recording_stopped_callback(const char *filepath, void *userdata) {
+ recorder_callbacks_context *context = userdata;
+ if(!filepath) {
+ printf("gsr error: Failed to save recording\n");
+ fflush(stdout);
+ gsr_ipc_complete_request(context->ipc, GSR_IPC_DEFERRED_REQUEST_STOP_REPLAY_RECORDING, false, NULL);
+ return;
+ }
+
+ puts(filepath);
+ fflush(stdout);
+ if(context->recording_saved_script)
+ run_recording_saved_script_async(context->recording_saved_script, filepath, "regular");
+
+ gsr_ipc_complete_request(context->ipc, GSR_IPC_DEFERRED_REQUEST_STOP_REPLAY_RECORDING, true, filepath);
+}
+
+#ifdef GSR_APP_AUDIO
+static gsr_pipewire_audio pipewire_audio;
+
+static bool app_audio_name_callback(const char *app_name, void *userdata) {
+ gsr_app_audio_names *app_audio_names = userdata;
+ gsr_app_audio_names_add(app_audio_names, app_name);
+ return true;
+}
+
+static int setup_app_audio(gsr_app_audio_names *app_audio_names) {
+ if(!pulseaudio_server_is_pipewire()) {
+ gsr_log(GSR_LOG_LEVEL_ERROR, "your sound server is not PipeWire. Application audio is only available when running PipeWire audio server");
+ return GSR_ERROR_UNSUPPORTED;
+ }
+
+ if(!gsr_pipewire_audio_init(&pipewire_audio)) {
+ gsr_log(GSR_LOG_LEVEL_ERROR, "failed to setup PipeWire audio for application audio capture");
+ return GSR_ERROR_UNSUPPORTED;
+ }
+
+ gsr_pipewire_audio_for_each_app(&pipewire_audio, app_audio_name_callback, app_audio_names);
+ return GSR_ERROR_OK;
+}
+#endif
+
+static int parse_audio_inputs(args_parser *arg_parser, gsr_audio_input_tracks *audio_input_tracks) {
+ const Arg *audio_input_arg = args_parser_get_arg(arg_parser, "-a");
+ assert(audio_input_arg);
+
+ gsr_audio_devices audio_devices;
+ memset(&audio_devices, 0, sizeof(audio_devices));
+ if(audio_input_arg->num_values > 0)
+ get_pulseaudio_inputs(&audio_devices);
+
+ const int parse_result = gsr_audio_input_tracks_parse(audio_input_tracks, audio_input_arg->values, audio_input_arg->num_values, &audio_devices);
+ gsr_audio_devices_deinit(&audio_devices);
+ return parse_result;
+}
+
+static int take_screenshot(args_parser *arg_parser, gsr_windowing *windowing, gsr_capture_deps *capture_deps, gsr_capture_sources *capture_sources, gsr_image_format image_format) {
+ const Arg *plugin_arg = args_parser_get_arg(arg_parser, "-p");
+ assert(plugin_arg);
+
+ arg_parser->settings.fps = 60; /* We want to capture an image as soon as possible */
+
+ gsr_screenshot_params screenshot_params;
+ memset(&screenshot_params, 0, sizeof(screenshot_params));
+ screenshot_params.settings = &arg_parser->settings;
+ screenshot_params.egl = &windowing->egl;
+ screenshot_params.window = windowing->window;
+ screenshot_params.capture_deps = capture_deps;
+ screenshot_params.capture_sources = capture_sources;
+ screenshot_params.image_format = image_format;
+ screenshot_params.plugin_filepaths = plugin_arg->values;
+ screenshot_params.num_plugin_filepaths = plugin_arg->num_values;
+ screenshot_params.running = &running;
+ screenshot_params.screenshot_saved = screenshot_saved_callback;
+ screenshot_params.userdata = (void*)arg_parser->settings.recording_saved_script;
+
+ return gsr_screenshot_take(&screenshot_params);
+}
+
+static int record(args_parser *arg_parser, gsr_windowing *windowing, gsr_capture_deps *capture_deps, gsr_capture_sources *capture_sources, gsr_audio_input_tracks *audio_input_tracks, gsr_ipc *ipc) {
+ const Arg *plugin_arg = args_parser_get_arg(arg_parser, "-p");
+ assert(plugin_arg);
+
+ gsr_recorder_params recorder_params;
+ memset(&recorder_params, 0, sizeof(recorder_params));
+ recorder_params.settings = &arg_parser->settings;
+ recorder_params.windowing = windowing;
+ recorder_params.capture_deps = capture_deps;
+ recorder_params.capture_sources = capture_sources;
+ recorder_params.audio_input_tracks = audio_input_tracks;
+ recorder_params.plugin_filepaths = plugin_arg->values;
+ recorder_params.num_plugin_filepaths = plugin_arg->num_values;
+#ifdef GSR_APP_AUDIO
+ recorder_params.pipewire_audio = &pipewire_audio;
+#endif
+
+ recorder_callbacks_context callbacks_context;
+ callbacks_context.recording_saved_script = arg_parser->settings.recording_saved_script;
+ callbacks_context.ipc = ipc;
+
+ gsr_recorder_callbacks callbacks;
+ memset(&callbacks, 0, sizeof(callbacks));
+ callbacks.replay_saved = replay_saved_callback;
+ callbacks.recording_started = recording_started_callback;
+ callbacks.recording_stopped = recording_stopped_callback;
+ callbacks.userdata = &callbacks_context;
+
+ int error = GSR_ERROR_OK;
+ recorder = gsr_recorder_create(&recorder_params, &callbacks, &error);
+ if(!recorder)
+ return error;
+
+ apply_pending_signals(recorder);
+ if(!atomic_load(&running))
+ gsr_recorder_stop(recorder);
+
+ gsr_ipc_handlers ipc_handlers;
+ memset(&ipc_handlers, 0, sizeof(ipc_handlers));
+ ipc_handlers.stop = ipc_stop_handler;
+ ipc_handlers.toggle_pause = ipc_toggle_pause_handler;
+ ipc_handlers.set_paused = ipc_set_paused_handler;
+ ipc_handlers.toggle_replay_recording = ipc_toggle_replay_recording_handler;
+ ipc_handlers.start_replay_recording = ipc_start_replay_recording_handler;
+ ipc_handlers.stop_replay_recording = ipc_stop_replay_recording_handler;
+ ipc_handlers.save_replay = ipc_save_replay_handler;
+ ipc_handlers.userdata = &arg_parser->settings;
+
+ int run_result = gsr_ipc_start(ipc, &ipc_handlers);
+ if(run_result == GSR_ERROR_OK)
+ run_result = gsr_recorder_run(recorder);
+
+ gsr_ipc_complete_request(ipc, GSR_IPC_DEFERRED_REQUEST_STOP, true, arg_parser->settings.is_replaying ? NULL : arg_parser->settings.filename);
+ gsr_ipc_stop(ipc);
+ gsr_recorder_destroy(recorder, true);
+ recorder = NULL;
+ return run_result;
+}
+
+static int run(args_parser *arg_parser) {
+ int exit_code = 0;
+
+ gsr_capture_sources capture_sources;
+ gsr_audio_input_tracks audio_input_tracks;
+ gsr_app_audio_names app_audio_names;
+ gsr_windowing windowing;
+ gsr_capture_deps capture_deps;
+ gsr_ipc ipc;
+ memset(&audio_input_tracks, 0, sizeof(audio_input_tracks));
+ memset(&app_audio_names, 0, sizeof(app_audio_names));
+ memset(&windowing, 0, sizeof(windowing));
+ memset(&ipc, 0, sizeof(ipc));
+ gsr_capture_deps_init(&capture_deps);
+
+ const Arg *ipc_arg = args_parser_get_arg(arg_parser, "-ipc");
+ assert(ipc_arg);
+
+ const int parse_capture_sources_result = gsr_capture_sources_parse(&capture_sources, arg_parser->settings.capture_source, arg_parser->settings.region_position, arg_parser->settings.region_size);
+ if(parse_capture_sources_result != GSR_ERROR_OK) {
+ exit_code = gsr_error_to_exit_code(parse_capture_sources_result);
+ goto done;
+ }
+
+ if(capture_sources.num_items == 0) {
+ gsr_log(GSR_LOG_LEVEL_ERROR, "option -w can't be empty. You need to capture video from at least one source");
+ args_parser_print_usage();
+ exit_code = 1;
+ goto done;
+ }
+
+ const int validate_args_result = validate_args_with_capture_sources(arg_parser, &capture_sources);
+ if(validate_args_result != GSR_ERROR_OK) {
+ exit_code = gsr_error_to_exit_code(validate_args_result);
+ goto done;
+ }
+
+ if(ipc_arg->num_values > 0 && gsr_ipc_init(&ipc, ipc_arg->values[0]) != GSR_ERROR_OK) {
+ exit_code = 1;
+ goto done;
+ }
+
+ const int parse_audio_inputs_result = parse_audio_inputs(arg_parser, &audio_input_tracks);
+ if(parse_audio_inputs_result != GSR_ERROR_OK) {
+ exit_code = gsr_error_to_exit_code(parse_audio_inputs_result);
+ goto done;
+ }
+
+ const bool uses_app_audio = gsr_audio_input_tracks_has_app_audio(&audio_input_tracks);
+#ifdef GSR_APP_AUDIO
+ if(uses_app_audio) {
+ const int app_audio_result = setup_app_audio(&app_audio_names);
+ if(app_audio_result != GSR_ERROR_OK) {
+ exit_code = gsr_error_to_exit_code(app_audio_result);
+ goto done;
+ }
+ }
+#else
+ if(uses_app_audio) {
+ gsr_log(GSR_LOG_LEVEL_ERROR, "application audio can't be recorded because GPU Screen Recorder is built without application audio support (-Dapp_audio option)");
+ exit_code = 2;
+ goto done;
+ }
+#endif
+
+ const int validate_app_audio_result = gsr_audio_input_tracks_validate_app_audio(&audio_input_tracks, &app_audio_names);
+ if(validate_app_audio_result != GSR_ERROR_OK) {
+ exit_code = gsr_error_to_exit_code(validate_app_audio_result);
+ goto done;
+ }
+
+ gsr_windowing_params windowing_params;
+ windowing_params.monitor_capture = gsr_capture_sources_has_monitor_or_region(&capture_sources);
+ windowing_params.gl_debug = arg_parser->settings.gl_debug;
+ windowing_params.listen_to_x11_events = true;
+ if(gsr_windowing_init(&windowing, &windowing_params) != GSR_ERROR_OK) {
+ exit_code = 1;
+ goto done;
+ }
+
+ if(gsr_capture_sources_has_type(&capture_sources, GSR_CAPTURE_SOURCE_TYPE_PORTAL)) {
+ if(gsr_windowing_is_using_prime_run()) {
+ gsr_log(GSR_LOG_LEVEL_WARNING, "use of prime-run with -w portal option is currently not supported. Disabling prime-run");
+ gsr_windowing_disable_prime_run();
+ }
+
+ if(video_codec_is_hdr(arg_parser->settings.video_codec)) {
+ gsr_log(GSR_LOG_LEVEL_WARNING, "portal capture option doesn't support hdr yet (PipeWire doesn't support hdr), the video will be tonemapped from hdr to sdr");
+ arg_parser->settings.video_codec = hdr_video_codec_to_sdr_video_codec(arg_parser->settings.video_codec);
+ }
+ }
+
+ if(gsr_windowing_load_egl(&windowing, &windowing_params) != GSR_ERROR_OK) {
+ exit_code = 1;
+ goto done;
+ }
+
+ gsr_shader_enable_debug_output(arg_parser->settings.gl_debug);
+#ifndef NDEBUG
+ gsr_shader_enable_debug_output(true);
+#endif
+
+ if(!args_parser_validate_with_gl_info(arg_parser, &windowing.egl)) {
+ exit_code = 1;
+ goto done;
+ }
+
+ if(!windowing.card_path_found) {
+ gsr_log(GSR_LOG_LEVEL_ERROR, "no /dev/dri/cardX device found. Make sure that you have at least one monitor connected or record a single window instead on X11 or record with the -w portal option");
+ exit_code = 2;
+ goto done;
+ }
+
+ gsr_capture_deps_init_cursor(&capture_deps, &windowing.egl, arg_parser->settings.record_cursor);
+
+ gsr_image_format image_format;
+ if(get_image_format_from_filename(arg_parser->settings.filename, &image_format)) {
+ if(audio_input_tracks.num_items > 0) {
+ gsr_log(GSR_LOG_LEVEL_ERROR, "can't record audio (-a) when taking a screenshot");
+ exit_code = 1;
+ goto done;
+ }
+
+ if(ipc_arg->num_values > 0)
+ gsr_log(GSR_LOG_LEVEL_WARNING, "option -ipc has no effect when taking a screenshot");
+
+ exit_code = gsr_error_to_exit_code(take_screenshot(arg_parser, &windowing, &capture_deps, &capture_sources, image_format));
+ } else {
+ exit_code = gsr_error_to_exit_code(record(arg_parser, &windowing, &capture_deps, &capture_sources, &audio_input_tracks, &ipc));
+ }
+
+ done:
+ gsr_ipc_deinit(&ipc);
+ gsr_capture_deps_deinit(&capture_deps);
+ gsr_windowing_deinit(&windowing);
+#ifdef GSR_APP_AUDIO
+ gsr_pipewire_audio_deinit(&pipewire_audio);
+#endif
+ gsr_app_audio_names_deinit(&app_audio_names);
+ gsr_audio_input_tracks_deinit(&audio_input_tracks);
+ gsr_capture_sources_deinit(&capture_sources);
+ return exit_code;
+}
+
+int main(int argc, char **argv) {
+ setlocale(LC_ALL, "C"); /* Sigh... stupid C */
+#ifdef __GLIBC__
+ mallopt(M_MMAP_THRESHOLD, 65536);
+#endif
+
+ install_signal_handlers();
+ set_environment_variables();
+ install_cuda_no_stable_perf_limit();
+
+ if(geteuid() == 0) {
+ gsr_log(GSR_LOG_LEVEL_ERROR, "don't run gpu-screen-recorder as the root user");
+ _exit(1);
+ }
+
+ args_handlers arg_handlers;
+ arg_handlers.version = version_command;
+ arg_handlers.info = info_command;
+ arg_handlers.list_audio_devices = list_audio_devices_command;
+ arg_handlers.list_application_audio = list_application_audio_command;
+ arg_handlers.list_v4l2_devices = list_v4l2_devices;
+ arg_handlers.list_capture_options = list_capture_options_command;
+ arg_handlers.list_monitors = list_monitors_command;
+
+ args_parser arg_parser;
+ int exit_code = 0;
+ int command_exit_code = 0;
+ switch(args_parser_parse(&arg_parser, argc, argv, &arg_handlers, NULL, &command_exit_code)) {
+ case ARGS_PARSE_RESULT_ERROR:
+ exit_code = 1;
+ break;
+ case ARGS_PARSE_RESULT_COMMAND_HANDLED:
+ exit_code = command_exit_code;
+ break;
+ case ARGS_PARSE_RESULT_OK: {
+ if(!arg_parser.settings.low_power) {
+ /* Forces low latency encoding mode. Use this environment variable until vaapi supports setting this as a parameter.
+ The downside of this is that it always uses maximum power, which is not ideal for replay mode that runs on system startup.
+ This option was added in mesa 24.1.4, released in july 17, 2024.
+ Seems like the performance issue is not in encoding, but rendering the frame.
+ Some frames end up taking 10 times longer. Seems to be an issue with amd gpu power management when letting the application sleep on the cpu side? */
+ setenv("AMD_DEBUG", "lowlatencyenc", true);
+ }
+
+ exit_code = run(&arg_parser);
+ break;
+ }
+ }
+
+ args_parser_deinit(&arg_parser);
+
+ /* We do an _exit here because cuda uses at_exit to do _something_ that causes the program to freeze,
+ but only on some nvidia driver versions on some gpus (RTX?), and _exit exits the program without calling
+ the at_exit registered functions.
+ Cuda (nvenc) is loaded in a separate process, but this still happens. */
+ _exit(exit_code);
+}