From e52278922dc7434227bc0d9840513f90a2a088e6 Mon Sep 17 00:00:00 2001 From: dec05eba Date: Mon, 27 Jul 2026 12:15:35 +0200 Subject: Fix gsr-kms-server not shutting down when gpu-screen-recorder is killed --- kms/client/kms_client.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kms/client/kms_client.c b/kms/client/kms_client.c index 0d4d963..25675ab 100644 --- a/kms/client/kms_client.c +++ b/kms/client/kms_client.c @@ -297,7 +297,7 @@ int gsr_kms_client_init(gsr_kms_client *self, const char *card_path) { #endif } - if(socketpair(AF_UNIX, SOCK_STREAM, 0, self->socket_pair) == -1) { + if(socketpair(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0, self->socket_pair) == -1) { fprintf(stderr, "gsr error: gsr_kms_client_init: socketpair failed, error: %s\n", strerror(errno)); goto err; } -- cgit v1.2.3 From aa69bdee8d3baf6819598304bb9e9a5a0c1a945c Mon Sep 17 00:00:00 2001 From: dec05eba Date: Mon, 27 Jul 2026 17:20:40 +0200 Subject: socket SOCK_CLOEXEC --- kms/client/kms_client.c | 2 +- kms/server/kms_server.c | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/kms/client/kms_client.c b/kms/client/kms_client.c index 25675ab..e37b4d5 100644 --- a/kms/client/kms_client.c +++ b/kms/client/kms_client.c @@ -302,7 +302,7 @@ int gsr_kms_client_init(gsr_kms_client *self, const char *card_path) { goto err; } - self->initial_socket_fd = socket(AF_UNIX, SOCK_STREAM, 0); + self->initial_socket_fd = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0); if(self->initial_socket_fd == -1) { fprintf(stderr, "gsr error: gsr_kms_client_init: socket failed, error: %s\n", strerror(errno)); goto err; diff --git a/kms/server/kms_server.c b/kms/server/kms_server.c index c41f8bd..4d9171f 100644 --- a/kms/server/kms_server.c +++ b/kms/server/kms_server.c @@ -509,7 +509,7 @@ int main(int argc, char **argv) { } const char *domain_socket_path = argv[1]; - socket_fd = socket(AF_UNIX, SOCK_STREAM, 0); + socket_fd = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0); if(socket_fd == -1) { fprintf(stderr, "kms server error: failed to create socket, error: %s\n", strerror(errno)); return 2; -- cgit v1.2.3 From 9c2c0e1d0de9b3968415b08830a45f64091d3b98 Mon Sep 17 00:00:00 2001 From: dec05eba Date: Thu, 30 Jul 2026 12:49:00 +0200 Subject: Improve manpage examples (with whip authorization) --- gpu-screen-recorder.1 | 47 +++++++++++++++++++++-------------------------- 1 file changed, 21 insertions(+), 26 deletions(-) diff --git a/gpu-screen-recorder.1 b/gpu-screen-recorder.1 index 38d4df6..edb855a 100644 --- a/gpu-screen-recorder.1 +++ b/gpu-screen-recorder.1 @@ -462,109 +462,104 @@ Use to send signals (e.g., .BR "pkill -SIGUSR1 -f ""^gpu-screen-recorder""" ). .SH EXAMPLES -Record monitor at 60 FPS with desktop audio: -.PP +.B Record monitor at 60 FPS with desktop audio: .nf .RS gpu-screen-recorder -w screen -f 60 -a default_output -o video.mp4 .RE .fi .PP -Record monitor at 60 FPS with desktop audio and microphone: -.PP +.B Record monitor at 60 FPS with desktop audio and microphone: .nf .RS gpu-screen-recorder -w screen -f 60 -a "default_output|default_input" -o video.mp4 .RE .fi .PP -Record specific window (X11 only): -.PP +.B Record specific window (X11 only): .nf .RS gpu-screen-recorder -w $(xdotool selectwindow) -f 60 -o video.mp4 .RE .fi .PP -Instant replay (last 60 seconds): -.PP +.B Instant replay (last 60 seconds): .nf .RS gpu-screen-recorder -w screen -c mp4 -r 60 -o ~/Videos .RE .fi .PP -Instant replay with constant framerate (recommended for predicable ram usage): -.PP +.B Instant replay with constant framerate (recommended for predicable ram usage): .nf .RS gpu-screen-recorder -w screen -c mp4 -r 60 -bm cbr -q 40000 -o ~/Videos .RE .fi .PP -Record region using slop: -.PP +.B Record region using slop: .nf .RS gpu-screen-recorder -w $(slop) -o video.mp4 .RE .fi .PP -Record region using slurp: -.PP +.B Record region using slurp: .nf .RS gpu-screen-recorder -w $(slurp -f "%wx%h+%x+%y") -o video.mp4 .RE .fi .PP -Instant replay and launch a script when saving replay: -.PP +.B Instant replay and launch a script when saving replay: .nf .RS gpu-screen-recorder -w screen -c mp4 -r 60 -sc ./script.sh -o ~/Videos .RE .fi .PP -Stream to Twitch with constant bitrate mode: -.PP +.B Stream to Twitch with constant bitrate mode: .nf .RS gpu-screen-recorder -w screen -c flv -a default_output -bm cbr -q 8000 -o "rtmp://live.twitch.tv/app/stream_key" .RE .fi .PP -Take screenshot: -.PP +.B Take screenshot: .nf .RS gpu-screen-recorder -w screen -o screenshot.jpg .PP .RE .fi -Record screen and camera: -.PP +.B Record screen and camera: .nf .RS gpu-screen-recorder -w "screen|/dev/video0" -o video.mp4 .PP .RE .fi -Record screen and camera. The camera is located at the bottom right and flipped horizontally: -.PP +.B Record screen and camera. The camera is located at the bottom right and flipped horizontally: .nf .RS gpu-screen-recorder -w "screen|/dev/video0;halign=end;valign=end;hflip=true;width=30%;height=30%" -o video.mp4 .PP .RE .fi -Record two monitors, side by side (assuming the first monitor has a resolution of 1920x1080) -.PP +.B Record two monitors, side by side (assuming the first monitor has a resolution of 1920x1080): .nf .RS gpu-screen-recorder -w "DP-1|DP-2;x=1920" -o video.mp4 .RE .fi +.PP +.B Stream whip with an authorization key: +.nf +.RS +gpu-screen-recorder -w screen -bm cbr -q 3500 -ffmpeg-opts "authorization=streamtest;ts_buffer_size=131072" -c whip -o https://b.siobud.com/api/whip +.PP +.RE +.fi .SH FILES .TP .I ~/.config/gpu-screen-recorder/gpu-screen-recorder.env -- cgit v1.2.3 From c150d0e6ba0ab1f9ecd6a1c933a58050187e0c81 Mon Sep 17 00:00:00 2001 From: dec05eba Date: Sat, 1 Aug 2026 18:51:59 +0200 Subject: Add gsr_log logging module with log level and overridable log handler --- include/log.h | 28 ++++ kms/client/kms_client.c | 61 ++++---- meson.build | 1 + src/args_parser.c | 103 ++++++------- src/capture/kms.c | 26 ++-- src/capture/nvfbc.c | 41 ++--- src/capture/portal.c | 53 +++---- src/capture/v4l2.c | 74 ++++----- src/capture/xcomposite.c | 13 +- src/capture/ximage.c | 11 +- src/codec_query/nvenc.c | 25 +-- src/codec_query/vaapi.c | 9 +- src/codec_query/vulkan.c | 27 ++-- src/color_conversion.c | 47 +++--- src/cuda.c | 13 +- src/cursor.c | 3 +- src/damage.c | 27 ++-- src/dbus.c | 115 +++++++------- src/egl.c | 43 +++--- src/encoder/encoder.c | 21 +-- src/encoder/video/nvenc.c | 18 +-- src/encoder/video/software.c | 7 +- src/encoder/video/vaapi.c | 38 ++--- src/encoder/video/vulkan.c | 38 ++--- src/image_writer.c | 7 +- src/kde_night_light.c | 3 +- src/library_loader.c | 3 +- src/log.c | 47 ++++++ src/main.cpp | 267 ++++++++++++++++----------------- src/pipewire_audio.c | 13 +- src/pipewire_video.c | 68 ++++----- src/plugins.c | 23 +-- src/replay_buffer/replay_buffer_disk.c | 17 ++- src/shader.c | 11 +- src/sound.cpp | 11 +- src/utils.c | 15 +- src/wayland_host_bridge.c | 7 +- src/window/wayland.c | 17 ++- src/window/x11.c | 5 +- 39 files changed, 723 insertions(+), 633 deletions(-) create mode 100644 include/log.h create mode 100644 src/log.c diff --git a/include/log.h b/include/log.h new file mode 100644 index 0000000..71be585 --- /dev/null +++ b/include/log.h @@ -0,0 +1,28 @@ +#ifndef GSR_LOG_H +#define GSR_LOG_H + +#ifdef __cplusplus +extern "C" { +#endif + +typedef enum { + GSR_LOG_LEVEL_DEBUG, + GSR_LOG_LEVEL_INFO, + GSR_LOG_LEVEL_WARNING, + GSR_LOG_LEVEL_ERROR +} gsr_log_level; + +/* The handler may be called from any thread. |message| has no prefix, level name or trailing newline */ +typedef void (*gsr_log_handler)(gsr_log_level level, const char *message, void *userdata); + +void gsr_log(gsr_log_level level, const char *fmt, ...) __attribute__((format(printf, 2, 3))); +/* Messages below |level| are discarded */ +void gsr_log_set_level(gsr_log_level level); +/* A NULL |handler| restores the default handler which prints "gsr : " to stderr */ +void gsr_log_set_handler(gsr_log_handler handler, void *userdata); + +#ifdef __cplusplus +} +#endif + +#endif /* GSR_LOG_H */ diff --git a/kms/client/kms_client.c b/kms/client/kms_client.c index e37b4d5..4d70d78 100644 --- a/kms/client/kms_client.c +++ b/kms/client/kms_client.c @@ -1,4 +1,5 @@ #include "kms_client.h" +#include "../../include/log.h" #include "../../include/utils.h" #include #include @@ -225,14 +226,14 @@ int gsr_kms_client_init(gsr_kms_client *self, const char *card_path) { struct sockaddr_un remote_addr = {0}; if(!create_socket_path(self->initial_socket_path, sizeof(self->initial_socket_path))) { - fprintf(stderr, "gsr error: gsr_kms_client_init: failed to create path to kms socket\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_kms_client_init: failed to create path to kms socket"); return -1; } char server_filepath[PATH_MAX]; #ifdef __linux__ if(!readlink_realpath("/proc/self/exe", server_filepath)) { - fprintf(stderr, "gsr error: gsr_kms_client_init: failed to resolve /proc/self/exe\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_kms_client_init: failed to resolve /proc/self/exe"); return -1; } @@ -241,7 +242,7 @@ int gsr_kms_client_init(gsr_kms_client *self, const char *card_path) { size_t size = PATH_MAX; if (sysctl(mib, 4, server_filepath, &size, NULL, 0) != 0) { - fprintf(stderr, "gsr error: gsr_kms_client_init: failed to resolve pathname using sysctl\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_kms_client_init: failed to resolve pathname using sysctl"); return -1; } @@ -251,19 +252,19 @@ int gsr_kms_client_init(gsr_kms_client *self, const char *card_path) { file_get_directory(server_filepath); if(!strcat_safe(server_filepath, sizeof(server_filepath), "/gsr-kms-server")) { - fprintf(stderr, "gsr error: gsr_kms_client_init: gsr-kms-server path too long\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_kms_client_init: gsr-kms-server path too long"); return -1; } if(access(server_filepath, F_OK) != 0) { - fprintf(stderr, "gsr info: gsr_kms_client_init: gsr-kms-server is not installed in the same directory as gpu-screen-recorder (%s not found), looking for gsr-kms-server in PATH instead\n", server_filepath); + gsr_log(GSR_LOG_LEVEL_INFO, "gsr_kms_client_init: gsr-kms-server is not installed in the same directory as gpu-screen-recorder (%s not found), looking for gsr-kms-server in PATH instead", server_filepath); if(!find_program_in_path("gsr-kms-server", server_filepath, sizeof(server_filepath)) || access(server_filepath, F_OK) != 0) { - fprintf(stderr, "gsr error: gsr_kms_client_init: gsr-kms-server was not found in PATH. Please install gpu-screen-recorder properly\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_kms_client_init: gsr-kms-server was not found in PATH. Please install gpu-screen-recorder properly"); return -1; } } - fprintf(stderr, "gsr info: gsr_kms_client_init: setting up connection to %s\n", server_filepath); + gsr_log(GSR_LOG_LEVEL_INFO, "gsr_kms_client_init: setting up connection to %s", server_filepath); const bool inside_flatpak = getenv("FLATPAK_ID") != NULL; const char *home = getenv("HOME"); @@ -288,23 +289,23 @@ int gsr_kms_client_init(gsr_kms_client *self, const char *card_path) { cap_free(kms_server_cap); } else if(!inside_flatpak) { if(errno == ENODATA) - fprintf(stderr, "gsr info: gsr_kms_client_init: gsr-kms-server is missing sys_admin cap and will require root authentication. To bypass this automatically, run: sudo setcap cap_sys_admin+ep '%s'\n", server_filepath); + gsr_log(GSR_LOG_LEVEL_INFO, "gsr_kms_client_init: gsr-kms-server is missing sys_admin cap and will require root authentication. To bypass this automatically, run: sudo setcap cap_sys_admin+ep '%s'", server_filepath); else - fprintf(stderr, "gsr info: gsr_kms_client_init: failed to get cap\n"); + gsr_log(GSR_LOG_LEVEL_INFO, "gsr_kms_client_init: failed to get cap"); } #else - fprintf(stderr, "gsr info: gsr_kms_client_init: platform doesn't support cap\n"); + gsr_log(GSR_LOG_LEVEL_INFO, "gsr_kms_client_init: platform doesn't support cap"); #endif } if(socketpair(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0, self->socket_pair) == -1) { - fprintf(stderr, "gsr error: gsr_kms_client_init: socketpair failed, error: %s\n", strerror(errno)); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_kms_client_init: socketpair failed, error: %s", strerror(errno)); goto err; } self->initial_socket_fd = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0); if(self->initial_socket_fd == -1) { - fprintf(stderr, "gsr error: gsr_kms_client_init: socket failed, error: %s\n", strerror(errno)); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_kms_client_init: socket failed, error: %s", strerror(errno)); goto err; } @@ -316,18 +317,18 @@ int gsr_kms_client_init(gsr_kms_client *self, const char *card_path) { umask(prev_mask); if(bind_res == -1) { - fprintf(stderr, "gsr error: gsr_kms_client_init: failed to bind socket, error: %s\n", strerror(errno)); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_kms_client_init: failed to bind socket, error: %s", strerror(errno)); goto err; } if(listen(self->initial_socket_fd, 1) == -1) { - fprintf(stderr, "gsr error: gsr_kms_client_init: failed to listen on socket, error: %s\n", strerror(errno)); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_kms_client_init: failed to listen on socket, error: %s", strerror(errno)); goto err; } pid_t pid = fork(); if(pid == -1) { - fprintf(stderr, "gsr error: gsr_kms_client_init: fork failed, error: %s\n", strerror(errno)); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_kms_client_init: fork failed, error: %s", strerror(errno)); goto err; } else if(pid == 0) { /* child */ if(inside_flatpak) { @@ -340,7 +341,7 @@ int gsr_kms_client_init(gsr_kms_client *self, const char *card_path) { const char *args[] = { "pkexec", server_filepath, self->initial_socket_path, card_path, NULL }; execvp(args[0], (char *const*)args); } - fprintf(stderr, "gsr error: gsr_kms_client_init: failed to launch \"gsr-kms-server\", error: %s\n", strerror(errno)); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_kms_client_init: failed to launch \"gsr-kms-server\", error: %s", strerror(errno)); _exit(127); } else { /* parent */ self->kms_server_pid = pid; @@ -348,7 +349,7 @@ int gsr_kms_client_init(gsr_kms_client *self, const char *card_path) { // We need this dumb-shit retardation with unix domain socket and then replace it with socketpair because // pkexec doesn't work with socketpair................ - fprintf(stderr, "gsr info: gsr_kms_client_init: waiting for server to connect\n"); + gsr_log(GSR_LOG_LEVEL_INFO, "gsr_kms_client_init: waiting for server to connect"); struct pollfd poll_fd = { .fd = self->initial_socket_fd, .events = POLLIN, @@ -360,7 +361,7 @@ int gsr_kms_client_init(gsr_kms_client *self, const char *card_path) { socklen_t sock_len = 0; self->initial_client_fd = accept(self->initial_socket_fd, (struct sockaddr*)&remote_addr, &sock_len); if(self->initial_client_fd == -1) { - fprintf(stderr, "gsr error: gsr_kms_client_init: accept failed on socket, error: %s\n", strerror(errno)); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_kms_client_init: accept failed on socket, error: %s", strerror(errno)); goto err; } break; @@ -371,7 +372,7 @@ int gsr_kms_client_init(gsr_kms_client *self, const char *card_path) { int exit_code = -1; if(WIFEXITED(status)) exit_code = WEXITSTATUS(status); - fprintf(stderr, "gsr error: gsr_kms_client_init: kms server died or never started, exit code: %d\n", exit_code); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_kms_client_init: kms server died or never started, exit code: %d", exit_code); self->kms_server_pid = -1; if(exit_code != 0) result = exit_code; @@ -380,14 +381,14 @@ int gsr_kms_client_init(gsr_kms_client *self, const char *card_path) { } poll_fd.revents = 0; } - fprintf(stderr, "gsr info: gsr_kms_client_init: server connected\n"); + gsr_log(GSR_LOG_LEVEL_INFO, "gsr_kms_client_init: server connected"); - fprintf(stderr, "gsr info: replacing file-backed unix domain socket with socketpair\n"); + gsr_log(GSR_LOG_LEVEL_INFO, "replacing file-backed unix domain socket with socketpair"); if(gsr_kms_client_replace_connection(self) != 0) goto err; cleanup_socket(self, false); - fprintf(stderr, "gsr info: using socketpair\n"); + gsr_log(GSR_LOG_LEVEL_INFO, "using socketpair"); return 0; @@ -445,21 +446,21 @@ int gsr_kms_client_replace_connection(gsr_kms_client *self) { request.type = KMS_REQUEST_TYPE_REPLACE_CONNECTION; request.new_connection_fd = self->socket_pair[GSR_SOCKET_PAIR_REMOTE]; if(send_msg_to_server(self->initial_client_fd, &request) == -1) { - fprintf(stderr, "gsr error: gsr_kms_client_replace_connection: failed to send request message to server\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_kms_client_replace_connection: failed to send request message to server"); return -1; } const int recv_res = recv_msg_from_server(self->kms_server_pid, self->socket_pair[GSR_SOCKET_PAIR_LOCAL], &response); if(recv_res == 0) { - fprintf(stderr, "gsr warning: gsr_kms_client_replace_connection: kms server shut down\n"); + gsr_log(GSR_LOG_LEVEL_WARNING, "gsr_kms_client_replace_connection: kms server shut down"); return -1; } else if(recv_res == -1) { - fprintf(stderr, "gsr error: gsr_kms_client_replace_connection: failed to receive response\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_kms_client_replace_connection: failed to receive response"); return -1; } if(response.version != GSR_KMS_PROTOCOL_VERSION) { - fprintf(stderr, "gsr error: gsr_kms_client_replace_connection: expected gsr-kms-server protocol version to be %u, but it's %u. please reinstall gpu screen recorder\n", GSR_KMS_PROTOCOL_VERSION, response.version); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_kms_client_replace_connection: expected gsr-kms-server protocol version to be %u, but it's %u. please reinstall gpu screen recorder", GSR_KMS_PROTOCOL_VERSION, response.version); /*close_fds(response);*/ return -1; } @@ -478,24 +479,24 @@ int gsr_kms_client_get_kms(gsr_kms_client *self, gsr_kms_response *response) { request.type = KMS_REQUEST_TYPE_GET_KMS; request.new_connection_fd = 0; if(send_msg_to_server(self->socket_pair[GSR_SOCKET_PAIR_LOCAL], &request) == -1) { - fprintf(stderr, "gsr error: gsr_kms_client_get_kms: failed to send request message to server\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_kms_client_get_kms: failed to send request message to server"); strcpy(response->err_msg, "failed to send"); return -1; } const int recv_res = recv_msg_from_server(self->kms_server_pid, self->socket_pair[GSR_SOCKET_PAIR_LOCAL], response); if(recv_res == 0) { - fprintf(stderr, "gsr warning: gsr_kms_client_get_kms: kms server shut down\n"); + gsr_log(GSR_LOG_LEVEL_WARNING, "gsr_kms_client_get_kms: kms server shut down"); strcpy(response->err_msg, "failed to receive"); return -1; } else if(recv_res == -1) { - fprintf(stderr, "gsr error: gsr_kms_client_get_kms: failed to receive response\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_kms_client_get_kms: failed to receive response"); strcpy(response->err_msg, "failed to receive"); return -1; } if(response->version != GSR_KMS_PROTOCOL_VERSION) { - fprintf(stderr, "gsr error: gsr_kms_client_get_kms: expected gsr-kms-server protocol version to be %u, but it's %u. please reinstall gpu screen recorder\n", GSR_KMS_PROTOCOL_VERSION, response->version); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_kms_client_get_kms: expected gsr-kms-server protocol version to be %u, but it's %u. please reinstall gpu screen recorder", GSR_KMS_PROTOCOL_VERSION, response->version); /*close_fds(response);*/ strcpy(response->err_msg, "mismatching protocol version"); return -1; diff --git a/meson.build b/meson.build index 4a79378..2bd947e 100644 --- a/meson.build +++ b/meson.build @@ -30,6 +30,7 @@ src = [ 'src/replay_buffer/replay_buffer.c', 'src/replay_buffer/replay_buffer_ram.c', 'src/replay_buffer/replay_buffer_disk.c', + 'src/log.c', 'src/egl.c', 'src/cuda.c', 'src/window_texture.c', diff --git a/src/args_parser.c b/src/args_parser.c index c37712a..039dfca 100644 --- a/src/args_parser.c +++ b/src/args_parser.c @@ -1,4 +1,5 @@ #include "../include/args_parser.h" +#include "../include/log.h" #include "../include/defs.h" #include "../include/egl.h" #include "../include/window/window.h" @@ -116,17 +117,18 @@ static bool arg_get_enum_value_by_name(const Arg *arg, const char *name, int *en return false; } -static void arg_print_expected_enum_names(const Arg *arg) { +static void arg_get_expected_enum_names(const Arg *arg, char *buf, size_t buf_size) { assert(arg->type == ARG_TYPE_ENUM); assert(arg->enum_values); + buf[0] = '\0'; + size_t offset = 0; for(int i = 0; i < arg->num_enum_values; ++i) { - if(i > 0) { - if(i == arg->num_enum_values -1) - fprintf(stderr, " or "); - else - fprintf(stderr, ", "); - } - fprintf(stderr, "'%s'", arg->enum_values[i].name); + const char *separator = ""; + if(i > 0) + separator = i == arg->num_enum_values - 1 ? " or " : ", "; + const int written = snprintf(buf + offset, offset < buf_size ? buf_size - offset : 0, "%s'%s'", separator, arg->enum_values[i].name); + if(written > 0) + offset += written; } } @@ -280,11 +282,11 @@ static bool args_parser_set_values(args_parser *self) { self->keyint = args_get_double_by_key(self->args, NUM_ARGS, "-keyint", 2.0); if(overclock) { - fprintf(stderr, "gsr info: the overclock option (-oc) is deprecated and no longer has any effect as it's no longer needed (on GPUs that are 12 years old or younger)\n"); + gsr_log(GSR_LOG_LEVEL_INFO, "the overclock option (-oc) is deprecated and no longer has any effect as it's no longer needed (on GPUs that are 12 years old or younger)"); } if(self->audio_codec == GSR_AUDIO_CODEC_FLAC) { - fprintf(stderr, "gsr warning: flac audio codec is temporary disabled, using opus audio codec instead\n"); + gsr_log(GSR_LOG_LEVEL_WARNING, "flac audio codec is temporary disabled, using opus audio codec instead"); self->audio_codec = GSR_AUDIO_CODEC_OPUS; } @@ -292,7 +294,7 @@ static bool args_parser_set_values(args_parser *self) { if(self->portal_session_token_filepath) { int len = strlen(self->portal_session_token_filepath); if(len > 0 && self->portal_session_token_filepath[len - 1] == '/') { - fprintf(stderr, "gsr error: -portal-session-token-filepath should be a path to a file but it ends with a /: %s\n", self->portal_session_token_filepath); + gsr_log(GSR_LOG_LEVEL_ERROR, "-portal-session-token-filepath should be a path to a file but it ends with a /: %s", self->portal_session_token_filepath); return false; } } @@ -301,13 +303,13 @@ static bool args_parser_set_values(args_parser *self) { if(self->recording_saved_script) { struct stat buf; if(stat(self->recording_saved_script, &buf) == -1 || !S_ISREG(buf.st_mode)) { - fprintf(stderr, "gsr error: Script \"%s\" either doesn't exist or it's not a file\n", self->recording_saved_script); + gsr_log(GSR_LOG_LEVEL_ERROR, "Script \"%s\" either doesn't exist or it's not a file", self->recording_saved_script); usage(); return false; } if(!(buf.st_mode & S_IXUSR)) { - fprintf(stderr, "gsr error: Script \"%s\" is not executable\n", self->recording_saved_script); + gsr_log(GSR_LOG_LEVEL_ERROR, "Script \"%s\" is not executable", self->recording_saved_script); usage(); return false; } @@ -319,19 +321,19 @@ static bool args_parser_set_values(args_parser *self) { if(self->bitrate_mode == GSR_BITRATE_MODE_CBR) { if(!quality_str) { - fprintf(stderr, "gsr error: option '-q' is required when using '-bm cbr' option\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "option '-q' is required when using '-bm cbr' option"); usage(); return false; } if(sscanf(quality_str, "%" PRIi64, &self->video_bitrate) != 1) { - fprintf(stderr, "gsr error: -q argument \"%s\" is not an integer value. When using '-bm cbr' option '-q' is expected to be an integer value\n", quality_str); + gsr_log(GSR_LOG_LEVEL_ERROR, "-q argument \"%s\" is not an integer value. When using '-bm cbr' option '-q' is expected to be an integer value", quality_str); usage(); return false; } if(self->video_bitrate < 0) { - fprintf(stderr, "gsr error: -q is expected to be 0 or larger, got %" PRIi64 "\n", self->video_bitrate); + gsr_log(GSR_LOG_LEVEL_ERROR, "-q is expected to be 0 or larger, got %" PRIi64, self->video_bitrate); usage(); return false; } @@ -350,7 +352,7 @@ static bool args_parser_set_values(args_parser *self) { } else if(strcmp(quality_str, "ultra") == 0) { self->video_quality = GSR_VIDEO_QUALITY_ULTRA; } else { - fprintf(stderr, "gsr error: -q should either be 'medium', 'high', 'very_high' or 'ultra', got: '%s'\n", quality_str); + gsr_log(GSR_LOG_LEVEL_ERROR, "-q should either be 'medium', 'high', 'very_high' or 'ultra', got: '%s'", quality_str); usage(); return false; } @@ -361,13 +363,13 @@ static bool args_parser_set_values(args_parser *self) { const char *output_resolution_str = args_get_value_by_key(self->args, NUM_ARGS, "-s"); if(output_resolution_str) { if(sscanf(output_resolution_str, "%dx%d", &self->output_resolution.x, &self->output_resolution.y) != 2) { - fprintf(stderr, "gsr error: invalid value for option -s '%s', expected a value in format WxH\n", output_resolution_str); + gsr_log(GSR_LOG_LEVEL_ERROR, "invalid value for option -s '%s', expected a value in format WxH", output_resolution_str); usage(); return false; } if(self->output_resolution.x < 0 || self->output_resolution.y < 0) { - fprintf(stderr, "gsr error: invalid value for option -s '%s', expected width and height to be greater or equal to 0\n", output_resolution_str); + gsr_log(GSR_LOG_LEVEL_ERROR, "invalid value for option -s '%s', expected width and height to be greater or equal to 0", output_resolution_str); usage(); return false; } @@ -378,13 +380,13 @@ static bool args_parser_set_values(args_parser *self) { const char *region_str = args_get_value_by_key(self->args, NUM_ARGS, "-region"); if(region_str) { if(sscanf(region_str, "%dx%d+%d+%d", &self->region_size.x, &self->region_size.y, &self->region_position.x, &self->region_position.y) != 4) { - fprintf(stderr, "gsr error: invalid value for option -region '%s', expected a value in format WxH+X+Y\n", region_str); + gsr_log(GSR_LOG_LEVEL_ERROR, "invalid value for option -region '%s', expected a value in format WxH+X+Y", region_str); usage(); return false; } if(self->region_size.x < 0 || self->region_size.y < 0) { - fprintf(stderr, "gsr error: invalid value for option -region '%s', expected width and height to be greater or equal to 0\n", region_str); + gsr_log(GSR_LOG_LEVEL_ERROR, "invalid value for option -region '%s', expected width and height to be greater or equal to 0", region_str); usage(); return false; } @@ -406,7 +408,7 @@ static bool args_parser_set_values(args_parser *self) { self->is_livestream = is_livestream_path(self->filename); if(self->is_livestream) { if(self->is_replaying) { - fprintf(stderr, "gsr error: replay mode is not applicable to live streaming\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "replay mode is not applicable to live streaming"); return false; } } else { @@ -416,20 +418,20 @@ static bool args_parser_set_values(args_parser *self) { char *directory = dirname(directory_buf); if(strcmp(directory, ".") != 0 && strcmp(directory, "/") != 0) { if(create_directory_recursive(directory) != 0) { - fprintf(stderr, "gsr error: failed to create directory for output file: %s\n", self->filename); + gsr_log(GSR_LOG_LEVEL_ERROR, "failed to create directory for output file: %s", self->filename); return false; } } } else { if(!self->container_format) { - fprintf(stderr, "gsr error: option -c is required when using option -r\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "option -c is required when using option -r"); usage(); return false; } struct stat buf; if(stat(self->filename, &buf) != -1 && !S_ISDIR(buf.st_mode)) { - fprintf(stderr, "gsr error: File \"%s\" exists but it's not a directory\n", self->filename); + gsr_log(GSR_LOG_LEVEL_ERROR, "File \"%s\" exists but it's not a directory", self->filename); usage(); return false; } @@ -439,13 +441,13 @@ static bool args_parser_set_values(args_parser *self) { if(!self->is_replaying) { self->filename = "/dev/stdout"; } else { - fprintf(stderr, "gsr error: Option -o is required when using option -r\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "Option -o is required when using option -r"); usage(); return false; } if(!self->container_format) { - fprintf(stderr, "gsr error: option -c is required when not using option -o\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "option -c is required when not using option -o"); usage(); return false; } @@ -454,14 +456,14 @@ static bool args_parser_set_values(args_parser *self) { self->is_output_piped = file_is_pipe_or_char_device(self->filename); self->low_latency_recording = self->is_livestream || self->is_output_piped; if(self->write_first_frame_ts && (self->is_livestream || self->is_output_piped)) { - fprintf(stderr, "gsr warning: -write-first-frame-ts is ignored for livestreaming or when output is piped\n"); + gsr_log(GSR_LOG_LEVEL_WARNING, "-write-first-frame-ts is ignored for livestreaming or when output is piped"); self->write_first_frame_ts = false; } self->replay_recording_directory = args_get_value_by_key(self->args, NUM_ARGS, "-ro"); if(self->is_livestream && self->recording_saved_script) { - fprintf(stderr, "gsr warning: live stream detected, -sc script is ignored\n"); + gsr_log(GSR_LOG_LEVEL_WARNING, "live stream detected, -sc script is ignored"); self->recording_saved_script = NULL; } @@ -515,7 +517,7 @@ bool args_parser_parse(args_parser *self, int argc, char **argv, const args_hand arg_handlers->list_capture_options(card_path, userdata); return true; } else { - fprintf(stderr, "gsr error: expected --list-capture-options to be called with either no extra arguments or 1 extra argument (card path)\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "expected --list-capture-options to be called with either no extra arguments or 1 extra argument (card path)"); return false; } } @@ -575,19 +577,19 @@ bool args_parser_parse(args_parser *self, int argc, char **argv, const args_hand const char *arg_name = argv[i]; Arg *arg = args_get_by_key(self->args, NUM_ARGS, arg_name); if(!arg) { - fprintf(stderr, "gsr error: invalid argument '%s'\n", arg_name); + gsr_log(GSR_LOG_LEVEL_ERROR, "invalid argument '%s'", arg_name); usage(); return false; } if(arg->num_values > 0 && !arg->list) { - fprintf(stderr, "gsr error: expected argument '%s' to only be specified once\n", arg_name); + gsr_log(GSR_LOG_LEVEL_ERROR, "expected argument '%s' to only be specified once", arg_name); usage(); return false; } if(i + 1 >= argc) { - fprintf(stderr, "gsr error: missing value for argument '%s'\n", arg_name); + gsr_log(GSR_LOG_LEVEL_ERROR, "missing value for argument '%s'", arg_name); usage(); return false; } @@ -603,7 +605,7 @@ bool args_parser_parse(args_parser *self, int argc, char **argv, const args_hand } else if(strcmp(arg_value, "no") == 0) { arg->typed_value.boolean = false; } else { - fprintf(stderr, "gsr error: %s should either be 'yes' or 'no', got: '%s'\n", arg_name, arg_value); + gsr_log(GSR_LOG_LEVEL_ERROR, "%s should either be 'yes' or 'no', got: '%s'", arg_name, arg_value); usage(); return false; } @@ -611,9 +613,9 @@ bool args_parser_parse(args_parser *self, int argc, char **argv, const args_hand } case ARG_TYPE_ENUM: { if(!arg_get_enum_value_by_name(arg, arg_value, &arg->typed_value.enum_value)) { - fprintf(stderr, "gsr error: %s should either be ", arg_name); - arg_print_expected_enum_names(arg); - fprintf(stderr, ", got: '%s'\n", arg_value); + char expected_enum_names[512]; + arg_get_expected_enum_names(arg, expected_enum_names, sizeof(expected_enum_names)); + gsr_log(GSR_LOG_LEVEL_ERROR, "%s should either be %s, got: '%s'", arg_name, expected_enum_names, arg_value); usage(); return false; } @@ -621,19 +623,19 @@ bool args_parser_parse(args_parser *self, int argc, char **argv, const args_hand } case ARG_TYPE_I64: { if(sscanf(arg_value, "%" PRIi64, &arg->typed_value.i64_value) != 1) { - fprintf(stderr, "gsr error: %s argument \"%s\" is not an integer\n", arg_name, arg_value); + gsr_log(GSR_LOG_LEVEL_ERROR, "%s argument \"%s\" is not an integer", arg_name, arg_value); usage(); return false; } if(arg->typed_value.i64_value < arg->integer_value_min) { - fprintf(stderr, "gsr error: %s argument is expected to be larger than %" PRIi64 ", got %" PRIi64 "\n", arg_name, arg->integer_value_min, arg->typed_value.i64_value); + gsr_log(GSR_LOG_LEVEL_ERROR, "%s argument is expected to be larger than %" PRIi64 ", got %" PRIi64, arg_name, arg->integer_value_min, arg->typed_value.i64_value); usage(); return false; } if(arg->typed_value.i64_value > arg->integer_value_max) { - fprintf(stderr, "gsr error: %s argument is expected to be less than %" PRIi64 ", got %" PRIi64 "\n", arg_name, arg->integer_value_max, arg->typed_value.i64_value); + gsr_log(GSR_LOG_LEVEL_ERROR, "%s argument is expected to be less than %" PRIi64 ", got %" PRIi64, arg_name, arg->integer_value_max, arg->typed_value.i64_value); usage(); return false; } @@ -641,19 +643,19 @@ bool args_parser_parse(args_parser *self, int argc, char **argv, const args_hand } case ARG_TYPE_DOUBLE: { if(sscanf(arg_value, "%lf", &arg->typed_value.d_value) != 1) { - fprintf(stderr, "gsr error: %s argument \"%s\" is not an floating-point number\n", arg_name, arg_value); + gsr_log(GSR_LOG_LEVEL_ERROR, "%s argument \"%s\" is not an floating-point number", arg_name, arg_value); usage(); return false; } if(arg->typed_value.d_value < arg->integer_value_min) { - fprintf(stderr, "gsr error: %s argument is expected to be larger than %" PRIi64 ", got %lf\n", arg_name, arg->integer_value_min, arg->typed_value.d_value); + gsr_log(GSR_LOG_LEVEL_ERROR, "%s argument is expected to be larger than %" PRIi64 ", got %lf", arg_name, arg->integer_value_min, arg->typed_value.d_value); usage(); return false; } if(arg->typed_value.d_value > arg->integer_value_max) { - fprintf(stderr, "gsr error: %s argument is expected to be less than %" PRIi64 ", got %lf\n", arg_name, arg->integer_value_max, arg->typed_value.d_value); + gsr_log(GSR_LOG_LEVEL_ERROR, "%s argument is expected to be less than %" PRIi64 ", got %lf", arg_name, arg->integer_value_max, arg->typed_value.d_value); usage(); return false; } @@ -662,7 +664,7 @@ bool args_parser_parse(args_parser *self, int argc, char **argv, const args_hand } if(!arg_append_value(arg, arg_value)) { - fprintf(stderr, "gsr error: failed to append argument, out of memory\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "failed to append argument, out of memory"); return false; } } @@ -670,7 +672,7 @@ bool args_parser_parse(args_parser *self, int argc, char **argv, const args_hand for(int i = 0; i < NUM_ARGS; ++i) { const Arg *arg = &self->args[i]; if(!arg->optional && arg->num_values == 0) { - fprintf(stderr, "gsr error: missing argument '%s'\n", arg->key); + gsr_log(GSR_LOG_LEVEL_ERROR, "missing argument '%s'", arg->key); usage(); return false; } @@ -694,28 +696,27 @@ bool args_parser_validate_with_gl_info(args_parser *self, gsr_egl *egl) { } if(egl->gpu_info.is_steam_deck && self->bitrate_mode == GSR_BITRATE_MODE_QP) { - fprintf(stderr, "gsr warning: qp bitrate mode is not supported on Steam Deck because of Steam Deck driver bugs. Using vbr instead\n"); + gsr_log(GSR_LOG_LEVEL_WARNING, "qp bitrate mode is not supported on Steam Deck because of Steam Deck driver bugs. Using vbr instead"); self->bitrate_mode = GSR_BITRATE_MODE_VBR; } if(self->video_encoder == GSR_VIDEO_ENCODER_HW_CPU && self->bitrate_mode == GSR_BITRATE_MODE_VBR) { - fprintf(stderr, "gsr warning: bitrate mode has been forcefully set to qp because software encoding option doesn't support vbr option\n"); + gsr_log(GSR_LOG_LEVEL_WARNING, "bitrate mode has been forcefully set to qp because software encoding option doesn't support vbr option"); self->bitrate_mode = GSR_BITRATE_MODE_QP; } if(egl->gpu_info.is_steam_deck) { - fprintf(stderr, "gsr warning: steam deck has multiple driver issues. One of them has been reported here: https://github.com/ValveSoftware/SteamOS/issues/1609\n" - "If you have issues with GPU Screen Recorder on steam deck that you don't have on a desktop computer then report the issue to Valve and/or AMD.\n"); + gsr_log(GSR_LOG_LEVEL_WARNING, "steam deck has multiple driver issues. One of them has been reported here: https://github.com/ValveSoftware/SteamOS/issues/1609\nIf you have issues with GPU Screen Recorder on steam deck that you don't have on a desktop computer then report the issue to Valve and/or AMD."); } self->very_old_gpu = false; if(egl->gpu_info.vendor == GSR_GPU_VENDOR_NVIDIA && egl->gpu_info.gpu_version != 0 && egl->gpu_info.gpu_version < 900) { - fprintf(stderr, "gsr info: your gpu appears to be very old (older than maxwell architecture). Switching to lower preset\n"); + gsr_log(GSR_LOG_LEVEL_INFO, "your gpu appears to be very old (older than maxwell architecture). Switching to lower preset"); self->very_old_gpu = true; } if(video_codec_is_hdr(self->video_codec) && !wayland) { - fprintf(stderr, "gsr error: hdr video codec option %s is not available on X11\n", video_codec_to_string(self->video_codec)); + gsr_log(GSR_LOG_LEVEL_ERROR, "hdr video codec option %s is not available on X11", video_codec_to_string(self->video_codec)); usage(); return false; } diff --git a/src/capture/kms.c b/src/capture/kms.c index 0f7c37c..4048a7f 100644 --- a/src/capture/kms.c +++ b/src/capture/kms.c @@ -1,4 +1,5 @@ #include "../../include/capture/kms.h" +#include "../../include/log.h" #include "../../include/utils.h" #include "../../include/color_conversion.h" #include "../../include/cursor.h" @@ -159,7 +160,7 @@ static void monitor_callback(const gsr_monitor *monitor, void *userdata) { } if(monitor_callback_userdata->monitor_id->num_connector_ids == MAX_CONNECTOR_IDS) - fprintf(stderr, "gsr warning: reached max connector ids\n"); + gsr_log(GSR_LOG_LEVEL_WARNING, "reached max connector ids"); } static vec2i rotate_capture_size_if_rotated(gsr_capture_kms *self, vec2i capture_size, gsr_monitor_rotation rotation) { @@ -191,7 +192,7 @@ static int gsr_capture_kms_start(gsr_capture *cap, gsr_capture_metadata *capture for_each_active_monitor_output(self->params.egl->window, self->params.egl->card_path, connection_type, monitor_callback, &monitor_callback_userdata); if(!get_monitor_by_name(self->params.egl, connection_type, self->params.display_to_capture, &monitor)) { - fprintf(stderr, "gsr error: gsr_capture_kms_start: failed to find monitor by name \"%s\"\n", self->params.display_to_capture); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_capture_kms_start: failed to find monitor by name \"%s\"", self->params.display_to_capture); gsr_capture_kms_stop(self); return -1; } @@ -454,7 +455,7 @@ static void gsr_capture_kms_update_hdr_color_transforms(gsr_capture_kms *self, g const bool night_light = self->params.kde_night_light && gsr_kde_night_light_get_inverse_matrix(self->params.kde_night_light, night_light_compensation, night_light_container_primaries, night_light_matrix); if(night_light && !self->night_light_message_shown) { self->night_light_message_shown = true; - fprintf(stderr, "gsr info: gsr_capture_kms_update_hdr_color_transforms: night light is active, removing the night light tint from the capture\n"); + gsr_log(GSR_LOG_LEVEL_INFO, "gsr_capture_kms_update_hdr_color_transforms: night light is active, removing the night light tint from the capture"); } /* The compositor applies the night light tint at scanout with the crtc gamma lut when it offloads its color transforms to the crtc @@ -468,24 +469,21 @@ static void gsr_capture_kms_update_hdr_color_transforms(gsr_capture_kms *self, g luminance_scale = SDR_WHITE_LUMINANCE_REFERENCE / sdr_white_luminance; if(luminance_scale != 1.0f && !self->hdr_luminance_message_shown) { self->hdr_luminance_message_shown = true; - fprintf(stderr, "gsr info: gsr_capture_kms_update_hdr_color_transforms: scaling the luminance of the hdr video from sdr white at %d nits to sdr white at %d nits (hdr peak luminance: %d nits)\n", - (int)sdr_white_luminance, (int)SDR_WHITE_LUMINANCE_REFERENCE, (int)(hdr_peak_luminance * luminance_scale)); + gsr_log(GSR_LOG_LEVEL_INFO, "gsr_capture_kms_update_hdr_color_transforms: scaling the luminance of the hdr video from sdr white at %d nits to sdr white at %d nits (hdr peak luminance: %d nits)", (int)sdr_white_luminance, (int)SDR_WHITE_LUMINANCE_REFERENCE, (int)(hdr_peak_luminance * luminance_scale)); } } const bool convert_sdr_to_hdr = self->params.hdr && !plane_is_hdr; if(convert_sdr_to_hdr && !self->hdr_luminance_message_shown) { self->hdr_luminance_message_shown = true; - fprintf(stderr, "gsr info: gsr_capture_kms_update_hdr_color_transforms: the monitor is in sdr mode, converting the captured sdr image to hdr (BT.709 to BT.2020 with sdr white at %d nits)\n", - (int)SDR_WHITE_LUMINANCE_REFERENCE); + gsr_log(GSR_LOG_LEVEL_INFO, "gsr_capture_kms_update_hdr_color_transforms: the monitor is in sdr mode, converting the captured sdr image to hdr (BT.709 to BT.2020 with sdr white at %d nits)", (int)SDR_WHITE_LUMINANCE_REFERENCE); } const bool tone_map_hdr_to_sdr = !self->params.hdr && plane_is_hdr; gsr_color_conversion_set_hdr_to_sdr_tone_mapping(color_conversion, tone_map_hdr_to_sdr, hdr_peak_luminance, sdr_white_luminance); if(tone_map_hdr_to_sdr && !self->tone_mapping_message_shown) { self->tone_mapping_message_shown = true; - fprintf(stderr, "gsr info: gsr_capture_kms_update_hdr_color_transforms: the monitor is in hdr mode, tone mapping the hdr content to sdr (sdr white luminance: %d nits, hdr peak luminance: %d nits). Record with -k hevc_hdr or -k av1_hdr video codec option to record hdr instead\n", - (int)sdr_white_luminance, (int)hdr_peak_luminance); + gsr_log(GSR_LOG_LEVEL_INFO, "gsr_capture_kms_update_hdr_color_transforms: the monitor is in hdr mode, tone mapping the hdr content to sdr (sdr white luminance: %d nits, hdr peak luminance: %d nits). Record with -k hevc_hdr or -k av1_hdr video codec option to record hdr instead", (int)sdr_white_luminance, (int)hdr_peak_luminance); } if(convert_sdr_to_hdr) { @@ -615,7 +613,7 @@ static EGLImage gsr_capture_kms_create_egl_image_with_fallback(gsr_capture_kms * } else { image = gsr_capture_kms_create_egl_image(self, drm_fd, fds, offsets, pitches, modifiers, true); if(!image) { - fprintf(stderr, "gsr error: gsr_capture_kms_create_egl_image_with_fallback: failed to create egl image with modifiers, trying without modifiers\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_capture_kms_create_egl_image_with_fallback: failed to create egl image with modifiers, trying without modifiers"); self->no_modifiers_fallback = true; image = gsr_capture_kms_create_egl_image(self, drm_fd, fds, offsets, pitches, modifiers, false); } @@ -638,7 +636,7 @@ static void gsr_capture_kms_bind_image_to_input_texture_with_fallback(gsr_captur gsr_capture_kms_bind_image_to_texture(self, image, self->external_input_texture_id, true); } else { if(!gsr_capture_kms_bind_image_to_texture(self, image, self->input_texture_id, false)) { - fprintf(stderr, "gsr error: gsr_capture_kms_capture: failed to bind image to texture, trying with external texture\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_capture_kms_capture: failed to bind image to texture, trying with external texture"); self->external_texture_fallback = true; gsr_capture_kms_bind_image_to_texture(self, image, self->external_input_texture_id, true); } @@ -824,7 +822,7 @@ static void gsr_capture_kms_update_connector_ids(gsr_capture_kms *self) { gsr_monitor monitor; if(!get_monitor_by_name(self->params.egl, connection_type, self->params.display_to_capture, &monitor)) { - fprintf(stderr, "gsr error: gsr_capture_kms_update_connector_ids: failed to find monitor by name \"%s\"\n", self->params.display_to_capture); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_capture_kms_update_connector_ids: failed to find monitor by name \"%s\"", self->params.display_to_capture); return; } @@ -851,7 +849,7 @@ static void gsr_capture_kms_pre_capture(gsr_capture *cap, gsr_capture_metadata * static bool error_shown = false; if(!error_shown) { error_shown = true; - fprintf(stderr, "gsr error: gsr_capture_kms_pre_capture: no drm found, capture will fail\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_capture_kms_pre_capture: no drm found, capture will fail"); } return; } @@ -1105,7 +1103,7 @@ static void gsr_capture_kms_destroy(gsr_capture *cap) { gsr_capture* gsr_capture_kms_create(const gsr_capture_kms_params *params) { if(!params) { - fprintf(stderr, "gsr error: gsr_capture_kms_create params is NULL\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_capture_kms_create params is NULL"); return NULL; } diff --git a/src/capture/nvfbc.c b/src/capture/nvfbc.c index 1504323..41e2466 100644 --- a/src/capture/nvfbc.c +++ b/src/capture/nvfbc.c @@ -1,4 +1,5 @@ #include "../../include/capture/nvfbc.h" +#include "../../include/log.h" #include "../../external/NvFBC.h" #include "../../include/egl.h" #include "../../include/utils.h" @@ -74,13 +75,13 @@ static bool gsr_capture_nvfbc_load_library(gsr_capture *cap) { dlerror(); /* clear */ void *lib = dlopen("libnvidia-fbc.so.1", RTLD_LAZY); if(!lib) { - fprintf(stderr, "gsr error: failed to load libnvidia-fbc.so.1, error: %s\n", dlerror()); + gsr_log(GSR_LOG_LEVEL_ERROR, "failed to load libnvidia-fbc.so.1, error: %s", dlerror()); return false; } set_func_ptr((void**)&self->nv_fbc_create_instance, dlsym(lib, "NvFBCCreateInstance")); if(!self->nv_fbc_create_instance) { - fprintf(stderr, "gsr error: unable to resolve symbol 'NvFBCCreateInstance'\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "unable to resolve symbol 'NvFBCCreateInstance'"); dlclose(lib); return false; } @@ -89,7 +90,7 @@ static bool gsr_capture_nvfbc_load_library(gsr_capture *cap) { self->nv_fbc_function_list.dwVersion = NVFBC_VERSION; NVFBCSTATUS status = self->nv_fbc_create_instance(&self->nv_fbc_function_list); if(status != NVFBC_SUCCESS) { - fprintf(stderr, "gsr error: failed to create NvFBC instance (status: %d)\n", status); + gsr_log(GSR_LOG_LEVEL_ERROR, "failed to create NvFBC instance (status: %d)", status); dlclose(lib); return false; } @@ -143,7 +144,7 @@ static int gsr_capture_nvfbc_setup_handle(gsr_capture_nvfbc *self) { status = self->nv_fbc_function_list.nvFBCCreateHandle(&self->nv_fbc_handle, &create_params); if(status != NVFBC_SUCCESS) { - fprintf(stderr, "gsr error: gsr_capture_nvfbc_start failed: %s\n", self->nv_fbc_function_list.nvFBCGetLastErrorStr(self->nv_fbc_handle)); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_capture_nvfbc_start failed: %s", self->nv_fbc_function_list.nvFBCGetLastErrorStr(self->nv_fbc_handle)); goto error_cleanup; } } @@ -155,12 +156,12 @@ static int gsr_capture_nvfbc_setup_handle(gsr_capture_nvfbc *self) { status = self->nv_fbc_function_list.nvFBCGetStatus(self->nv_fbc_handle, &status_params); if(status != NVFBC_SUCCESS) { - fprintf(stderr, "gsr error: gsr_capture_nvfbc_start failed: %s\n", self->nv_fbc_function_list.nvFBCGetLastErrorStr(self->nv_fbc_handle)); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_capture_nvfbc_start failed: %s", self->nv_fbc_function_list.nvFBCGetLastErrorStr(self->nv_fbc_handle)); goto error_cleanup; } if(status_params.bCanCreateNow == NVFBC_FALSE) { - fprintf(stderr, "gsr error: gsr_capture_nvfbc_start failed: it's not possible to create a capture session on this system\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_capture_nvfbc_start failed: it's not possible to create a capture session on this system"); goto error_cleanup; } @@ -172,18 +173,18 @@ static int gsr_capture_nvfbc_setup_handle(gsr_capture_nvfbc *self) { self->tracking_type = strcmp(self->params.display_to_capture, "screen") == 0 ? NVFBC_TRACKING_SCREEN : NVFBC_TRACKING_OUTPUT; if(self->tracking_type == NVFBC_TRACKING_OUTPUT) { if(!status_params.bXRandRAvailable) { - fprintf(stderr, "gsr error: gsr_capture_nvfbc_start failed: the xrandr extension is not available\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_capture_nvfbc_start failed: the xrandr extension is not available"); goto error_cleanup; } if(status_params.bInModeset) { - fprintf(stderr, "gsr error: gsr_capture_nvfbc_start failed: the x server is in modeset, unable to record\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_capture_nvfbc_start failed: the x server is in modeset, unable to record"); goto error_cleanup; } self->output_id = get_output_id_from_display_name(status_params.outputs, status_params.dwOutputNum, self->params.display_to_capture, &self->tracking_width, &self->tracking_height); if(self->output_id == 0) { - fprintf(stderr, "gsr error: gsr_capture_nvfbc_start failed: display '%s' not found\n", self->params.display_to_capture); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_capture_nvfbc_start failed: display '%s' not found", self->params.display_to_capture); goto error_cleanup; } } @@ -215,7 +216,7 @@ static int gsr_capture_nvfbc_setup_session(gsr_capture_nvfbc *self) { NVFBCSTATUS status = self->nv_fbc_function_list.nvFBCCreateCaptureSession(self->nv_fbc_handle, &create_capture_params); if(status != NVFBC_SUCCESS) { - fprintf(stderr, "gsr error: gsr_capture_nvfbc_start failed: %s\n", self->nv_fbc_function_list.nvFBCGetLastErrorStr(self->nv_fbc_handle)); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_capture_nvfbc_start failed: %s", self->nv_fbc_function_list.nvFBCGetLastErrorStr(self->nv_fbc_handle)); return -1; } self->capture_session_created = true; @@ -226,7 +227,7 @@ static int gsr_capture_nvfbc_setup_session(gsr_capture_nvfbc *self) { status = self->nv_fbc_function_list.nvFBCToGLSetUp(self->nv_fbc_handle, &self->setup_params); if(status != NVFBC_SUCCESS) { - fprintf(stderr, "gsr error: gsr_capture_nvfbc_start failed: %s\n", self->nv_fbc_function_list.nvFBCGetLastErrorStr(self->nv_fbc_handle)); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_capture_nvfbc_start failed: %s", self->nv_fbc_function_list.nvFBCGetLastErrorStr(self->nv_fbc_handle)); gsr_capture_nvfbc_destroy_session(self); return -1; } @@ -256,12 +257,12 @@ static int gsr_capture_nvfbc_start(gsr_capture *cap, gsr_capture_metadata *captu int driver_major_version = 0; int driver_minor_version = 0; if(self->params.direct_capture && get_nvidia_driver_version(&driver_major_version, &driver_minor_version)) { - fprintf(stderr, "gsr info: detected nvidia version: %d.%d\n", driver_major_version, driver_minor_version); + gsr_log(GSR_LOG_LEVEL_INFO, "detected nvidia version: %d.%d", driver_major_version, driver_minor_version); // TODO: if(version_at_least(driver_major_version, driver_minor_version, 515, 57) && version_less_than(driver_major_version, driver_minor_version, 520, 56)) { self->params.direct_capture = false; - fprintf(stderr, "gsr warning: \"screen-direct\" has temporary been disabled as it causes stuttering with driver versions >= 515.57 and < 520.56. Please update your driver if possible. Capturing \"screen\" instead.\n"); + gsr_log(GSR_LOG_LEVEL_WARNING, "\"screen-direct\" has temporary been disabled as it causes stuttering with driver versions >= 515.57 and < 520.56. Please update your driver if possible. Capturing \"screen\" instead."); } // TODO: @@ -271,7 +272,7 @@ static int gsr_capture_nvfbc_start(gsr_capture *cap, gsr_capture_metadata *captu if(version_at_least(driver_major_version, driver_minor_version, 515, 57)) self->supports_direct_cursor = true; else - fprintf(stderr, "gsr info: capturing \"screen-direct\" but driver version appears to be less than 515.57. Disabling capture of cursor. Please update your driver if you want to capture your cursor or record \"screen\" instead.\n"); + gsr_log(GSR_LOG_LEVEL_INFO, "capturing \"screen-direct\" but driver version appears to be less than 515.57. Disabling capture of cursor. Please update your driver if you want to capture your cursor or record \"screen\" instead."); } */ } @@ -349,16 +350,16 @@ static int gsr_capture_nvfbc_capture(gsr_capture *cap, gsr_capture_metadata *cap gsr_capture_nvfbc_destroy_session_and_handle(self); if(gsr_capture_nvfbc_setup_handle(self) != 0) { - fprintf(stderr, "gsr error: gsr_capture_nvfbc_capture failed to recreate nvfbc handle, trying again in %f second(s)\n", nvfbc_recreate_retry_time_seconds); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_capture_nvfbc_capture failed to recreate nvfbc handle, trying again in %f second(s)", nvfbc_recreate_retry_time_seconds); return -1; } if(gsr_capture_nvfbc_setup_session(self) != 0) { - fprintf(stderr, "gsr error: gsr_capture_nvfbc_capture failed to recreate nvfbc session, trying again in %f second(s)\n", nvfbc_recreate_retry_time_seconds); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_capture_nvfbc_capture failed to recreate nvfbc session, trying again in %f second(s)", nvfbc_recreate_retry_time_seconds); return -1; } - fprintf(stderr, "gsr info: gsr_capture_nvfbc_capture: recreated nvfbc session after modeset recovery\n"); + gsr_log(GSR_LOG_LEVEL_INFO, "gsr_capture_nvfbc_capture: recreated nvfbc session after modeset recovery"); self->nvfbc_needs_recreate = false; } else { return 0; @@ -385,7 +386,7 @@ static int gsr_capture_nvfbc_capture(gsr_capture *cap, gsr_capture_metadata *cap NVFBCSTATUS status = self->nv_fbc_function_list.nvFBCToGLGrabFrame(self->nv_fbc_handle, &grab_params); if(status != NVFBC_SUCCESS) { - fprintf(stderr, "gsr error: gsr_capture_nvfbc_capture failed: %s (%d), recreating session after %f second(s)\n", self->nv_fbc_function_list.nvFBCGetLastErrorStr(self->nv_fbc_handle), status, nvfbc_recreate_retry_time_seconds); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_capture_nvfbc_capture failed: %s (%d), recreating session after %f second(s)", self->nv_fbc_function_list.nvFBCGetLastErrorStr(self->nv_fbc_handle), status, nvfbc_recreate_retry_time_seconds); self->nvfbc_needs_recreate = true; self->nvfbc_dead_start = clock_get_monotonic_seconds(); return 0; @@ -414,12 +415,12 @@ static void gsr_capture_nvfbc_destroy(gsr_capture *cap) { gsr_capture* gsr_capture_nvfbc_create(const gsr_capture_nvfbc_params *params) { if(!params) { - fprintf(stderr, "gsr error: gsr_capture_nvfbc_create params is NULL\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_capture_nvfbc_create params is NULL"); return NULL; } if(!params->display_to_capture) { - fprintf(stderr, "gsr error: gsr_capture_nvfbc_create params.display_to_capture is NULL\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_capture_nvfbc_create params.display_to_capture is NULL"); return NULL; } diff --git a/src/capture/portal.c b/src/capture/portal.c index fd1caec..622cf4a 100644 --- a/src/capture/portal.c +++ b/src/capture/portal.c @@ -1,4 +1,5 @@ #include "../../include/capture/portal.h" +#include "../../include/log.h" #include "../../include/color_conversion.h" #include "../../include/egl.h" #include "../../include/utils.h" @@ -111,7 +112,7 @@ static bool create_directory_to_file(const char *filepath) { snprintf(dir, sizeof(dir), "%.*s", (int)(split - filepath), filepath); if(create_directory_recursive(dir) != 0) { - fprintf(stderr, "gsr warning: gsr_capture_portal_save_restore_token: failed to create directory (%s) for restore token\n", dir); + gsr_log(GSR_LOG_LEVEL_WARNING, "gsr_capture_portal_save_restore_token: failed to create directory (%s) for restore token", dir); return false; } return true; @@ -130,18 +131,18 @@ static void gsr_capture_portal_save_restore_token(const char *restore_token, con FILE *f = fopen(restore_token_path, "wb"); if(!f) { - fprintf(stderr, "gsr warning: gsr_capture_portal_save_restore_token: failed to create restore token file (%s)\n", restore_token_path); + gsr_log(GSR_LOG_LEVEL_WARNING, "gsr_capture_portal_save_restore_token: failed to create restore token file (%s)", restore_token_path); return; } const int restore_token_len = strlen(restore_token); if((long)fwrite(restore_token, 1, restore_token_len, f) != restore_token_len) { - fprintf(stderr, "gsr warning: gsr_capture_portal_save_restore_token: failed to write restore token to file (%s)\n", restore_token_path); + gsr_log(GSR_LOG_LEVEL_WARNING, "gsr_capture_portal_save_restore_token: failed to write restore token to file (%s)", restore_token_path); fclose(f); return; } - fprintf(stderr, "gsr info: gsr_capture_portal_save_restore_token: saved restore token to cache (%s)\n", restore_token); + gsr_log(GSR_LOG_LEVEL_INFO, "gsr_capture_portal_save_restore_token: saved restore token to cache (%s)", restore_token); fclose(f); } @@ -158,7 +159,7 @@ static void gsr_capture_portal_get_restore_token_from_cache(char *buffer, size_t FILE *f = fopen(restore_token_path, "rb"); if(!f) { - fprintf(stderr, "gsr info: gsr_capture_portal_get_restore_token_from_cache: no restore token found in cache or failed to load (%s)\n", restore_token_path); + gsr_log(GSR_LOG_LEVEL_INFO, "gsr_capture_portal_get_restore_token_from_cache: no restore token found in cache or failed to load (%s)", restore_token_path); return; } @@ -168,7 +169,7 @@ static void gsr_capture_portal_get_restore_token_from_cache(char *buffer, size_t if(file_size > 0 && file_size < 1024 && file_size < (long)buffer_size && (long)fread(buffer, 1, file_size, f) != file_size) { buffer[0] = '\0'; - fprintf(stderr, "gsr warning: gsr_capture_portal_get_restore_token_from_cache: failed to read restore token (%s)\n", restore_token_path); + gsr_log(GSR_LOG_LEVEL_WARNING, "gsr_capture_portal_get_restore_token_from_cache: failed to read restore token (%s)", restore_token_path); fclose(f); return; } @@ -176,7 +177,7 @@ static void gsr_capture_portal_get_restore_token_from_cache(char *buffer, size_t if(file_size > 0 && file_size < (long)buffer_size) buffer[file_size] = '\0'; - fprintf(stderr, "gsr info: gsr_capture_portal_get_restore_token_from_cache: read cached restore token (%s)\n", buffer); + gsr_log(GSR_LOG_LEVEL_INFO, "gsr_capture_portal_get_restore_token_from_cache: read cached restore token (%s)", buffer); fclose(f); } @@ -193,24 +194,24 @@ static int gsr_capture_portal_setup_dbus(gsr_capture_portal *self, int *pipewire if(!gsr_dbus_init(&self->dbus, restore_token)) return -1; - fprintf(stderr, "gsr info: gsr_capture_portal_setup_dbus: CreateSession\n"); + gsr_log(GSR_LOG_LEVEL_INFO, "gsr_capture_portal_setup_dbus: CreateSession"); response_status = gsr_dbus_screencast_create_session(&self->dbus, &self->session_handle); if(response_status != 0) { - fprintf(stderr, "gsr error: gsr_capture_portal_setup_dbus: CreateSession failed\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_capture_portal_setup_dbus: CreateSession failed"); return response_status; } - fprintf(stderr, "gsr info: gsr_capture_portal_setup_dbus: SelectSources\n"); + gsr_log(GSR_LOG_LEVEL_INFO, "gsr_capture_portal_setup_dbus: SelectSources"); response_status = gsr_dbus_screencast_select_sources(&self->dbus, self->session_handle, GSR_PORTAL_CAPTURE_TYPE_ALL, self->params.record_cursor ? GSR_PORTAL_CURSOR_MODE_METADATA : GSR_PORTAL_CURSOR_MODE_HIDDEN); if(response_status != 0) { - fprintf(stderr, "gsr error: gsr_capture_portal_setup_dbus: SelectSources failed\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_capture_portal_setup_dbus: SelectSources failed"); return response_status; } - fprintf(stderr, "gsr info: gsr_capture_portal_setup_dbus: Start\n"); + gsr_log(GSR_LOG_LEVEL_INFO, "gsr_capture_portal_setup_dbus: Start"); response_status = gsr_dbus_screencast_start(&self->dbus, self->session_handle, pipewire_node); if(response_status != 0) { - fprintf(stderr, "gsr error: gsr_capture_portal_setup_dbus: Start failed\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_capture_portal_setup_dbus: Start failed"); return response_status; } @@ -218,31 +219,31 @@ static int gsr_capture_portal_setup_dbus(gsr_capture_portal *self, int *pipewire if(screencast_restore_token) gsr_capture_portal_save_restore_token(screencast_restore_token, self->params.portal_session_token_filepath); - fprintf(stderr, "gsr info: gsr_capture_portal_setup_dbus: OpenPipeWireRemote\n"); + gsr_log(GSR_LOG_LEVEL_INFO, "gsr_capture_portal_setup_dbus: OpenPipeWireRemote"); if(!gsr_dbus_screencast_open_pipewire_remote(&self->dbus, self->session_handle, pipewire_fd)) { - fprintf(stderr, "gsr error: gsr_capture_portal_setup_dbus: OpenPipeWireRemote failed\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_capture_portal_setup_dbus: OpenPipeWireRemote failed"); return -1; } - fprintf(stderr, "gsr info: gsr_capture_portal_setup_dbus: desktop portal setup finished\n"); + gsr_log(GSR_LOG_LEVEL_INFO, "gsr_capture_portal_setup_dbus: desktop portal setup finished"); return 0; } static bool gsr_capture_portal_get_frame_dimensions(gsr_capture_portal *self) { - fprintf(stderr, "gsr info: gsr_capture_portal_start: waiting for pipewire negotiation\n"); + gsr_log(GSR_LOG_LEVEL_INFO, "gsr_capture_portal_start: waiting for pipewire negotiation"); const double start_time = clock_get_monotonic_seconds(); while(clock_get_monotonic_seconds() - start_time < 5.0) { if(gsr_pipewire_video_map_texture(&self->pipewire, self->texture_map, &self->pipewire_data)) { self->capture_size.x = self->pipewire_data.region.width; self->capture_size.y = self->pipewire_data.region.height; - fprintf(stderr, "gsr info: gsr_capture_portal_start: pipewire negotiation finished\n"); + gsr_log(GSR_LOG_LEVEL_INFO, "gsr_capture_portal_start: pipewire negotiation finished"); return true; } usleep(30 * 1000); /* 30 milliseconds */ } - fprintf(stderr, "gsr info: gsr_capture_portal_start: timed out waiting for pipewire negotiation (5 seconds)\n"); + gsr_log(GSR_LOG_LEVEL_INFO, "gsr_capture_portal_start: timed out waiting for pipewire negotiation (5 seconds)"); return false; } @@ -259,24 +260,24 @@ static int gsr_capture_portal_setup(gsr_capture_portal *self, int fps) { // 2: The user interaction was ended in some other way // Response status value 2 happens usually if there was some kind of error in the desktop portal on the system if(response_status == 2) { - fprintf(stderr, "gsr error: gsr_capture_portal_setup: desktop portal capture failed. Either you Wayland compositor doesn't support desktop portal capture or it's incorrectly setup on your system\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_capture_portal_setup: desktop portal capture failed. Either you Wayland compositor doesn't support desktop portal capture or it's incorrectly setup on your system"); return 50; } else if(response_status == 1) { - fprintf(stderr, "gsr error: gsr_capture_portal_setup: desktop portal capture failed. It seems like desktop portal capture was canceled by the user.\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_capture_portal_setup: desktop portal capture failed. It seems like desktop portal capture was canceled by the user."); return PORTAL_CAPTURE_CANCELED_BY_USER_EXIT_CODE; } else { return -1; } } - fprintf(stderr, "gsr info: gsr_capture_portal_setup: setting up pipewire\n"); + gsr_log(GSR_LOG_LEVEL_INFO, "gsr_capture_portal_setup: setting up pipewire"); /* TODO: support hdr when pipewire supports it */ /* gsr_pipewire closes the pipewire fd, even on failure */ if(!gsr_pipewire_video_init(&self->pipewire, pipewire_fd, pipewire_node, fps, self->params.record_cursor, self->params.egl)) { - fprintf(stderr, "gsr error: gsr_capture_portal_setup: failed to setup pipewire with fd: %d, node: %" PRIu32 "\n", pipewire_fd, pipewire_node); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_capture_portal_setup: failed to setup pipewire with fd: %d, node: %" PRIu32, pipewire_fd, pipewire_node); return -1; } - fprintf(stderr, "gsr info: gsr_capture_portal_setup: pipewire setup finished\n"); + gsr_log(GSR_LOG_LEVEL_INFO, "gsr_capture_portal_setup: pipewire setup finished"); if(!gsr_capture_portal_get_frame_dimensions(self)) return -1; @@ -329,7 +330,7 @@ static void gsr_capture_portal_pre_capture(gsr_capture *cap, gsr_capture_metadat return; if(gsr_pipewire_video_should_restart(&self->pipewire)) { - fprintf(stderr, "gsr info: gsr_capture_portal_pre_capture: pipewire capture was paused, trying to start capture again\n"); + gsr_log(GSR_LOG_LEVEL_INFO, "gsr_capture_portal_pre_capture: pipewire capture was paused, trying to start capture again"); gsr_capture_portal_stop(self); const int result = gsr_capture_portal_setup(self, capture_metadata->fps); if(result != 0) { @@ -452,7 +453,7 @@ static void gsr_capture_portal_destroy(gsr_capture *cap) { gsr_capture* gsr_capture_portal_create(const gsr_capture_portal_params *params) { if(!params) { - fprintf(stderr, "gsr error: gsr_capture_portal_create params is NULL\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_capture_portal_create params is NULL"); return NULL; } diff --git a/src/capture/v4l2.c b/src/capture/v4l2.c index a4e81e4..22f0f59 100644 --- a/src/capture/v4l2.c +++ b/src/capture/v4l2.c @@ -1,4 +1,5 @@ #include "../../include/capture/v4l2.h" +#include "../../include/log.h" #include "../../include/color_conversion.h" #include "../../include/egl.h" #include "../../include/utils.h" @@ -367,12 +368,12 @@ static void gsr_capture_v4l2_update_params(int fd) { .type = V4L2_BUF_TYPE_VIDEO_CAPTURE, }; if(xioctl(fd, VIDIOC_G_PARM, &streamparm) == -1) { - fprintf(stderr, "gsr error: gsr_capture_v4l2_set_framerate: VIDIOC_G_PARM failed, error: %s\n", strerror(errno)); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_capture_v4l2_set_framerate: VIDIOC_G_PARM failed, error: %s", strerror(errno)); return; } if(xioctl(fd, VIDIOC_S_PARM, &streamparm) == -1) { - fprintf(stderr, "gsr error: gsr_capture_v4l2_set_framerate: VIDIOC_S_PARM failed, error: %s\n", strerror(errno)); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_capture_v4l2_set_framerate: VIDIOC_S_PARM failed, error: %s", strerror(errno)); return; } } @@ -382,19 +383,19 @@ static void gsr_capture_v4l2_set_framerate(int fd, gsr_capture_v4l2_framerate fr .type = V4L2_BUF_TYPE_VIDEO_CAPTURE, }; if(xioctl(fd, VIDIOC_G_PARM, &streamparm) == -1) { - fprintf(stderr, "gsr error: gsr_capture_v4l2_set_framerate: VIDIOC_G_PARM failed, error: %s\n", strerror(errno)); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_capture_v4l2_set_framerate: VIDIOC_G_PARM failed, error: %s", strerror(errno)); return; } streamparm.parm.capture.timeperframe.denominator = framerate.denominator; streamparm.parm.capture.timeperframe.numerator = framerate.numerator; if(xioctl(fd, VIDIOC_S_PARM, &streamparm) == -1) { - fprintf(stderr, "gsr error: gsr_capture_v4l2_set_framerate: VIDIOC_S_PARM failed, error: %s\n", strerror(errno)); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_capture_v4l2_set_framerate: VIDIOC_S_PARM failed, error: %s", strerror(errno)); return; } if(streamparm.parm.capture.timeperframe.denominator == 0 || streamparm.parm.capture.timeperframe.numerator == 0) { - fprintf(stderr, "gsr error: gsr_capture_v4l2_set_framerate: VIDIOC_S_PARM failed, error: invalid framerate: %u/%u\n", framerate.denominator, framerate.numerator);; + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_capture_v4l2_set_framerate: VIDIOC_S_PARM failed, error: invalid framerate: %u/%u", framerate.denominator, framerate.numerator);; return; } } @@ -403,21 +404,21 @@ static bool gsr_capture_v4l2_validate_pixfmt(const gsr_capture_v4l2 *self, const switch(self->params.pixfmt) { case GSR_CAPTURE_V4L2_PIXFMT_AUTO: { if(!supported_pixfmts.yuyv && !supported_pixfmts.mjpeg) { - fprintf(stderr, "gsr error: gsr_capture_v4l2_create: %s doesn't support yuyv nor mjpeg. GPU Screen Recorder supports only yuyv and mjpeg at the moment. Report this as an issue, see: https://git.dec05eba.com/?p=about\n", self->params.device_path); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_capture_v4l2_create: %s doesn't support yuyv nor mjpeg. GPU Screen Recorder supports only yuyv and mjpeg at the moment. Report this as an issue, see: https://git.dec05eba.com/?p=about", self->params.device_path); return false; } break; } case GSR_CAPTURE_V4L2_PIXFMT_YUYV: { if(!supported_pixfmts.yuyv) { - fprintf(stderr, "gsr error: gsr_capture_v4l2_create: %s doesn't support yuyv. Try recording with pixfmt=mjpeg or pixfmt=auto instead\n", self->params.device_path); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_capture_v4l2_create: %s doesn't support yuyv. Try recording with pixfmt=mjpeg or pixfmt=auto instead", self->params.device_path); return false; } break; } case GSR_CAPTURE_V4L2_PIXFMT_MJPEG: { if(!supported_pixfmts.mjpeg) { - fprintf(stderr, "gsr error: gsr_capture_v4l2_create: %s doesn't support mjpeg. Try recording with pixfmt=yuyv or pixfmt=auto instead\n", self->params.device_path); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_capture_v4l2_create: %s doesn't support mjpeg. Try recording with pixfmt=yuyv or pixfmt=auto instead", self->params.device_path); return false; } break; @@ -432,7 +433,7 @@ static bool gsr_capture_v4l2_create_pbos(gsr_capture_v4l2 *self, int width, int self->params.egl->glGenBuffers(NUM_PBOS, self->pbos); for(int i = 0; i < NUM_PBOS; ++i) { if(self->pbos[i] == 0) { - fprintf(stderr, "gsr error: gsr_capture_v4l2_create_pbos: failed to create pixel buffer objects\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_capture_v4l2_create_pbos: failed to create pixel buffer objects"); return false; } @@ -477,7 +478,7 @@ static bool gsr_capture_v4l2_map_buffer(gsr_capture_v4l2 *self, const struct v4l }); if(!self->dma_image[i]) { - fprintf(stderr, "gsr error: gsr_capture_v4l2_map_buffer: eglCreateImage failed, error: %d\n", self->params.egl->eglGetError()); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_capture_v4l2_map_buffer: eglCreateImage failed, error: %d", self->params.egl->eglGetError()); return false; } } @@ -488,7 +489,7 @@ static bool gsr_capture_v4l2_map_buffer(gsr_capture_v4l2 *self, const struct v4l self->params.egl->glTexParameteri(GL_TEXTURE_EXTERNAL_OES, GL_TEXTURE_MAG_FILTER, GL_LINEAR); self->params.egl->glBindTexture(GL_TEXTURE_EXTERNAL_OES, 0); if(self->texture_id[i] == 0) { - fprintf(stderr, "gsr error: gsr_capture_v4l2_map_buffer: failed to create texture\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_capture_v4l2_map_buffer: failed to create texture"); return false; } } @@ -501,14 +502,14 @@ static bool gsr_capture_v4l2_map_buffer(gsr_capture_v4l2 *self, const struct v4l self->dmabuf_size[i] = fmt->fmt.pix.sizeimage; self->dmabuf_map[i] = mmap(NULL, fmt->fmt.pix.sizeimage, PROT_READ, MAP_SHARED, self->dmabuf_fd[i], 0); if(self->dmabuf_map[i] == MAP_FAILED) { - fprintf(stderr, "gsr error: gsr_capture_v4l2_map_buffer: mmap failed, error: %s\n", strerror(errno)); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_capture_v4l2_map_buffer: mmap failed, error: %s", strerror(errno)); return false; } // GL_RGBA is intentionally used here instead of GL_RGB, because the performance is much better when using glTexSubImage2D (22% cpu usage compared to 38% cpu usage) self->texture_id[i] = gl_create_texture(self->params.egl, fmt->fmt.pix.width, fmt->fmt.pix.height, GL_RGBA8, GL_RGBA, GL_LINEAR); if(self->texture_id[i] == 0) { - fprintf(stderr, "gsr error: gsr_capture_v4l2_map_buffer: failed to create texture\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_capture_v4l2_map_buffer: failed to create texture"); return false; } } @@ -534,28 +535,28 @@ static bool is_libturbojpeg_library_available(void) { static int gsr_capture_v4l2_setup(gsr_capture_v4l2 *self) { self->fd = open(self->params.device_path, O_RDWR | O_NONBLOCK); if(self->fd < 0) { - fprintf(stderr, "gsr error: gsr_capture_v4l2_create: failed to open %s, error: %s\n", self->params.device_path, strerror(errno)); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_capture_v4l2_create: failed to open %s, error: %s", self->params.device_path, strerror(errno)); return -1; } struct v4l2_capability cap = {0}; if(xioctl(self->fd, VIDIOC_QUERYCAP, &cap) == -1) { if(EINVAL == errno) { - fprintf(stderr, "gsr error: gsr_capture_v4l2_create: %s isn't a v4l2 device\n", self->params.device_path); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_capture_v4l2_create: %s isn't a v4l2 device", self->params.device_path); return -1; } else { - fprintf(stderr, "gsr error: gsr_capture_v4l2_create: VIDIOC_QUERYCAP failed, error: %s\n", strerror(errno)); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_capture_v4l2_create: VIDIOC_QUERYCAP failed, error: %s", strerror(errno)); return -1; } } if(!(cap.capabilities & V4L2_CAP_VIDEO_CAPTURE)) { - fprintf(stderr, "gsr error: gsr_capture_v4l2_create: %s isn't a video capture device\n", self->params.device_path); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_capture_v4l2_create: %s isn't a video capture device", self->params.device_path); return -1; } if(!(cap.capabilities & V4L2_CAP_STREAMING)) { - fprintf(stderr, "gsr error: gsr_capture_v4l2_create: %s doesn't support streaming i/o\n", self->params.device_path); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_capture_v4l2_create: %s doesn't support streaming i/o", self->params.device_path); return -1; } @@ -563,7 +564,7 @@ static int gsr_capture_v4l2_setup(gsr_capture_v4l2 *self) { const bool has_libturbojpeg_lib = is_libturbojpeg_library_available(); if(!has_libturbojpeg_lib && self->params.pixfmt == GSR_CAPTURE_V4L2_PIXFMT_AUTO) { - fprintf(stderr, "gsr warning: gsr_capture_v4l2_create: libturbojpeg.so.0 isn't available on the system, yuyv camera capture will be used\n"); + gsr_log(GSR_LOG_LEVEL_WARNING, "gsr_capture_v4l2_create: libturbojpeg.so.0 isn't available on the system, yuyv camera capture will be used"); self->params.pixfmt = GSR_CAPTURE_V4L2_PIXFMT_YUYV; } @@ -576,12 +577,11 @@ static int gsr_capture_v4l2_setup(gsr_capture_v4l2 *self) { gsr_capture_v4l2_supported_setup best_supported_setup = {0}; if(!gsr_capture_v4l2_get_best_matching_setup(supported_setups, num_supported_setups, self->params.pixfmt, self->params.camera_fps, self->params.camera_resolution, &best_supported_setup)) { - fprintf(stderr, "gsr error: gsr_capture_v4l2_create: %s doesn't report any frame resolutions and framerates\n", self->params.device_path); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_capture_v4l2_create: %s doesn't report any frame resolutions and framerates", self->params.device_path); return -1; } - fprintf(stderr, "gsr info: gsr_capture_v4l2_create: capturing %s at %ux%u@%dhz, pixfmt: %s\n", - self->params.device_path, + gsr_log(GSR_LOG_LEVEL_INFO, "gsr_capture_v4l2_create: capturing %s at %ux%u@%dhz, pixfmt: %s", self->params.device_path, best_supported_setup.resolution.width, best_supported_setup.resolution.height, gsr_capture_v4l2_framerate_to_number(best_supported_setup.framerate), @@ -594,7 +594,7 @@ static int gsr_capture_v4l2_setup(gsr_capture_v4l2 *self) { dlerror(); /* clear */ self->libturbojpeg_lib = dlopen("libturbojpeg.so.0", RTLD_LAZY); if(!self->libturbojpeg_lib) { - fprintf(stderr, "gsr error: gsr_capture_v4l2_create: failed to load libturbojpeg.so.0 which is required for camera mjpeg capture, error: %s\n", dlerror()); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_capture_v4l2_create: failed to load libturbojpeg.so.0 which is required for camera mjpeg capture, error: %s", dlerror()); return -1; } @@ -605,13 +605,13 @@ static int gsr_capture_v4l2_setup(gsr_capture_v4l2 *self) { self->tjGetErrorStr2 = (FUNC_tjGetErrorStr2)dlsym(self->libturbojpeg_lib, "tjGetErrorStr2"); if(!self->tjInitDecompress || !self->tjDestroy || !self->tjDecompressHeader2 || !self->tjDecompress2 || !self->tjGetErrorStr2) { - fprintf(stderr, "gsr error: gsr_capture_v4l2_create: libturbojpeg.so.0 is missing functions. The libturbojpeg version installed on your system might be outdated\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_capture_v4l2_create: libturbojpeg.so.0 is missing functions. The libturbojpeg version installed on your system might be outdated"); return -1; } self->jpeg_decompressor = self->tjInitDecompress(); if(!self->jpeg_decompressor) { - fprintf(stderr, "gsr error: gsr_capture_v4l2_create: failed to create jpeg decompressor\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_capture_v4l2_create: failed to create jpeg decompressor"); return -1; } } @@ -624,12 +624,12 @@ static int gsr_capture_v4l2_setup(gsr_capture_v4l2 *self) { .fmt.pix.height = best_supported_setup.resolution.height, }; if(xioctl(self->fd, VIDIOC_S_FMT, &fmt) == -1) { - fprintf(stderr, "gsr error: gsr_capture_v4l2_create: VIDIOC_S_FMT failed, error: %s\n", strerror(errno)); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_capture_v4l2_create: VIDIOC_S_FMT failed, error: %s", strerror(errno)); return -1; } if(fmt.fmt.pix.pixelformat != v4l2_pixfmt) { - fprintf(stderr, "gsr error: gsr_capture_v4l2_create: pixel format isn't as requested (got pixel format: %u, requested: %u), error: %s\n", fmt.fmt.pix.pixelformat, v4l2_pixfmt, strerror(errno)); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_capture_v4l2_create: pixel format isn't as requested (got pixel format: %u, requested: %u), error: %s", fmt.fmt.pix.pixelformat, v4l2_pixfmt, strerror(errno)); return -1; } @@ -644,7 +644,7 @@ static int gsr_capture_v4l2_setup(gsr_capture_v4l2 *self) { .count = NUM_BUFFERS }; if(xioctl(self->fd, VIDIOC_REQBUFS, &reqbuf) == -1) { - fprintf(stderr, "gsr error: gsr_capture_v4l2_create: VIDIOC_REQBUFS failed, error: %s\n", strerror(errno)); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_capture_v4l2_create: VIDIOC_REQBUFS failed, error: %s", strerror(errno)); return -1; } @@ -655,7 +655,7 @@ static int gsr_capture_v4l2_setup(gsr_capture_v4l2 *self) { .flags = O_RDONLY }; if(xioctl(self->fd, VIDIOC_EXPBUF, &expbuf) == -1) { - fprintf(stderr, "gsr error: gsr_capture_v4l2_create: VIDIOC_EXPBUF failed, error: %s\n", strerror(errno)); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_capture_v4l2_create: VIDIOC_EXPBUF failed, error: %s", strerror(errno)); return -1; } self->dmabuf_fd[i] = expbuf.fd; @@ -674,11 +674,11 @@ static int gsr_capture_v4l2_setup(gsr_capture_v4l2 *self) { } if(xioctl(self->fd, VIDIOC_STREAMON, &(enum v4l2_buf_type){V4L2_BUF_TYPE_VIDEO_CAPTURE})) { - fprintf(stderr, "gsr error: gsr_capture_v4l2_create: VIDIOC_STREAMON failed, error: %s\n", strerror(errno)); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_capture_v4l2_create: VIDIOC_STREAMON failed, error: %s", strerror(errno)); return -1; } - fprintf(stderr, "gsr info: gsr_capture_v4l2_create: waiting for camera %s to be ready\n", self->params.device_path); + gsr_log(GSR_LOG_LEVEL_INFO, "gsr_capture_v4l2_create: waiting for camera %s to be ready", self->params.device_path); return 0; } @@ -707,7 +707,7 @@ static void gsr_capture_v4l2_tick(gsr_capture *cap) { if(!self->got_first_frame && !self->should_stop) { const double timeout_sec = 5.0; if(clock_get_monotonic_seconds() - self->capture_start_time >= timeout_sec) { - fprintf(stderr, "gsr error: gsr_capture_v4l2_capture: didn't receive camera data in %f seconds\n", timeout_sec); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_capture_v4l2_capture: didn't receive camera data in %f seconds", timeout_sec); self->should_stop = true; self->stop_is_error = true; } @@ -719,12 +719,12 @@ static void gsr_capture_v4l2_decode_jpeg_to_texture(gsr_capture_v4l2 *self, cons int jpeg_width = 0; int jpeg_height = 0; if(self->tjDecompressHeader2(self->jpeg_decompressor, self->dmabuf_map[buf->index], buf->bytesused, &jpeg_width, &jpeg_height, &jpeg_subsamp) != 0) { - fprintf(stderr, "gsr error: gsr_capture_v4l2_capture: failed to decompress camera jpeg header data, error: %s\n", self->tjGetErrorStr2(self->jpeg_decompressor)); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_capture_v4l2_capture: failed to decompress camera jpeg header data, error: %s", self->tjGetErrorStr2(self->jpeg_decompressor)); return; } if(jpeg_width != self->capture_size.x || jpeg_height != self->capture_size.y) { - fprintf(stderr, "gsr error: gsr_capture_v4l2_capture: got jpeg data of incorrect dimensions. Expected %dx%d, got %dx%d\n", self->capture_size.x, self->capture_size.y, jpeg_width, jpeg_height); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_capture_v4l2_capture: got jpeg data of incorrect dimensions. Expected %dx%d, got %dx%d", self->capture_size.x, self->capture_size.y, jpeg_width, jpeg_height); return; } @@ -742,7 +742,7 @@ static void gsr_capture_v4l2_decode_jpeg_to_texture(gsr_capture_v4l2 *self, cons void *mapped_buffer = self->params.egl->glMapBufferRange(GL_PIXEL_UNPACK_BUFFER, 0, self->capture_size.x * self->capture_size.y * 4, GL_MAP_WRITE_BIT); if(mapped_buffer) { if(self->tjDecompress2(self->jpeg_decompressor, self->dmabuf_map[buf->index], buf->bytesused, mapped_buffer, jpeg_width, 0, jpeg_height, TJPF_RGBA, TJFLAG_FASTDCT) != 0) - fprintf(stderr, "gsr error: gsr_capture_v4l2_capture: failed to decompress camera jpeg data, error: %s\n", self->tjGetErrorStr2(self->jpeg_decompressor)); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_capture_v4l2_capture: failed to decompress camera jpeg data, error: %s", self->tjGetErrorStr2(self->jpeg_decompressor)); self->params.egl->glUnmapBuffer(GL_PIXEL_UNPACK_BUFFER); } @@ -764,7 +764,7 @@ static int gsr_capture_v4l2_capture(gsr_capture *cap, gsr_capture_metadata *capt if(buf.bytesused > 0 && !(buf.flags & V4L2_BUF_FLAG_ERROR)) { if(!self->got_first_frame) - fprintf(stderr, "gsr info: gsr_capture_v4l2_capture: camera %s is now ready\n", self->params.device_path); + gsr_log(GSR_LOG_LEVEL_INFO, "gsr_capture_v4l2_capture: camera %s is now ready", self->params.device_path); self->got_first_frame = true; switch(self->buffer_type) { @@ -852,7 +852,7 @@ static void gsr_capture_v4l2_destroy(gsr_capture *cap) { gsr_capture* gsr_capture_v4l2_create(const gsr_capture_v4l2_params *params) { if(!params) { - fprintf(stderr, "gsr error: gsr_capture_v4l2_create params is NULL\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_capture_v4l2_create params is NULL"); return NULL; } diff --git a/src/capture/xcomposite.c b/src/capture/xcomposite.c index c1bef4f..af48698 100644 --- a/src/capture/xcomposite.c +++ b/src/capture/xcomposite.c @@ -1,4 +1,5 @@ #include "../../include/capture/xcomposite.h" +#include "../../include/log.h" #include "../../include/window_texture.h" #include "../../include/utils.h" #include "../../include/cursor.h" @@ -63,7 +64,7 @@ static int gsr_capture_xcomposite_start(gsr_capture *cap, gsr_capture_metadata * if(self->params.follow_focused) { self->net_active_window_atom = XInternAtom(self->display, "_NET_ACTIVE_WINDOW", False); if(!self->net_active_window_atom) { - fprintf(stderr, "gsr error: gsr_capture_xcomposite_start failed: failed to get _NET_ACTIVE_WINDOW atom\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_capture_xcomposite_start failed: failed to get _NET_ACTIVE_WINDOW atom"); return -1; } self->window = get_focused_window(self->display, self->net_active_window_atom); @@ -75,7 +76,7 @@ static int gsr_capture_xcomposite_start(gsr_capture *cap, gsr_capture_metadata * XWindowAttributes attr; if(!XGetWindowAttributes(self->display, self->window, &attr) && !self->params.follow_focused) { - fprintf(stderr, "gsr error: gsr_capture_xcomposite_start failed: invalid window id: %lu\n", self->window); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_capture_xcomposite_start failed: invalid window id: %lu", self->window); return -1; } @@ -89,7 +90,7 @@ static int gsr_capture_xcomposite_start(gsr_capture *cap, gsr_capture_metadata * XSelectInput(self->display, self->window, StructureNotifyMask | ExposureMask); if(window_texture_init(&self->window_texture, self->display, self->window, self->params.egl) != 0 && !self->params.follow_focused) { - fprintf(stderr, "gsr error: gsr_capture_xcomposite_start: failed to get window texture for window %ld\n", (long)self->window); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_capture_xcomposite_start: failed to get window texture for window %ld", (long)self->window); return -1; } @@ -129,7 +130,7 @@ static void gsr_capture_xcomposite_tick(gsr_capture *cap) { attr.width = 0; attr.height = 0; if(!XGetWindowAttributes(self->display, self->window, &attr)) - fprintf(stderr, "gsr error: gsr_capture_xcomposite_tick failed: invalid window id: %lu\n", self->window); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_capture_xcomposite_tick failed: invalid window id: %lu", self->window); self->window_pos.x = attr.x; self->window_pos.y = attr.y; @@ -153,7 +154,7 @@ static void gsr_capture_xcomposite_tick(gsr_capture *cap) { self->window_resized = false; if(window_texture_on_resize(&self->window_texture) != 0) { - fprintf(stderr, "gsr error: gsr_capture_xcomposite_tick: window_texture_on_resize failed\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_capture_xcomposite_tick: window_texture_on_resize failed"); //self->should_stop = true; //self->stop_is_error = true; return; @@ -291,7 +292,7 @@ static void gsr_capture_xcomposite_destroy(gsr_capture *cap) { gsr_capture* gsr_capture_xcomposite_create(const gsr_capture_xcomposite_params *params) { if(!params) { - fprintf(stderr, "gsr error: gsr_capture_xcomposite_create params is NULL\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_capture_xcomposite_create params is NULL"); return NULL; } diff --git a/src/capture/ximage.c b/src/capture/ximage.c index b0cf364..becca82 100644 --- a/src/capture/ximage.c +++ b/src/capture/ximage.c @@ -1,4 +1,5 @@ #include "../../include/capture/ximage.h" +#include "../../include/log.h" #include "../../include/utils.h" #include "../../include/cursor.h" #include "../../include/color_conversion.h" @@ -39,7 +40,7 @@ static int gsr_capture_ximage_start(gsr_capture *cap, gsr_capture_metadata *capt self->root_window = DefaultRootWindow(self->display); if(!get_monitor_by_name(self->params.egl, GSR_CONNECTION_X11, self->params.display_to_capture, &self->monitor)) { - fprintf(stderr, "gsr error: gsr_capture_ximage_start: failed to find monitor by name \"%s\"\n", self->params.display_to_capture); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_capture_ximage_start: failed to find monitor by name \"%s\"", self->params.display_to_capture); gsr_capture_ximage_stop(self); return -1; } @@ -61,7 +62,7 @@ static int gsr_capture_ximage_start(gsr_capture *cap, gsr_capture_metadata *capt self->texture_id = gl_create_texture(self->params.egl, self->capture_size.x, self->capture_size.y, GL_RGB8, GL_RGB, GL_LINEAR); if(self->texture_id == 0) { - fprintf(stderr, "gsr error: gsr_capture_ximage_start: failed to create texture\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_capture_ximage_start: failed to create texture"); gsr_capture_ximage_stop(self); return -1; } @@ -95,14 +96,14 @@ static bool gsr_capture_ximage_upload_to_texture(gsr_capture_ximage *self, int x XImage *image = XGetImage(self->display, self->root_window, x, y, width, height, AllPlanes, ZPixmap); if(!image) { - fprintf(stderr, "gsr error: gsr_capture_ximage_upload_to_texture: XGetImage failed\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_capture_ximage_upload_to_texture: XGetImage failed"); return false; } bool success = false; uint8_t *image_data = malloc(image->width * image->height * 3); if(!image_data) { - fprintf(stderr, "gsr error: gsr_capture_ximage_upload_to_texture: failed to allocate image data\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_capture_ximage_upload_to_texture: failed to allocate image data"); goto done; } @@ -186,7 +187,7 @@ static void gsr_capture_ximage_destroy(gsr_capture *cap) { gsr_capture* gsr_capture_ximage_create(const gsr_capture_ximage_params *params) { if(!params) { - fprintf(stderr, "gsr error: gsr_capture_ximage_create params is NULL\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_capture_ximage_create params is NULL"); return NULL; } diff --git a/src/codec_query/nvenc.c b/src/codec_query/nvenc.c index 37c25ba..3f8b442 100644 --- a/src/codec_query/nvenc.c +++ b/src/codec_query/nvenc.c @@ -1,4 +1,5 @@ #include "../../include/codec_query/nvenc.h" +#include "../../include/log.h" #include "../../include/cuda.h" #include "../../external/nvEncodeAPI.h" @@ -17,7 +18,7 @@ static void* open_nvenc_library(void) { if(!lib) { lib = dlopen("libnvidia-encode.so", RTLD_LAZY); if(!lib) { - fprintf(stderr, "gsr error: gsr_get_supported_video_codecs_nvenc failed: failed to load libnvidia-encode.so/libnvidia-encode.so.1, error: %s\n", dlerror()); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_get_supported_video_codecs_nvenc failed: failed to load libnvidia-encode.so/libnvidia-encode.so.1, error: %s", dlerror()); return NULL; } } @@ -107,7 +108,7 @@ static bool encoder_get_supported_profiles(const NV_ENCODE_API_FUNCTION_LIST *fu uint32_t profile_guid_count = 0; if(function_list->nvEncGetEncodeProfileGUIDCount(nvenc_encoder, *encoder_guid, &profile_guid_count) != NV_ENC_SUCCESS) { - fprintf(stderr, "gsr error: gsr_get_supported_video_codecs_nvenc: nvEncGetEncodeProfileGUIDCount failed, error: %s\n", function_list->nvEncGetLastErrorString(nvenc_encoder)); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_get_supported_video_codecs_nvenc: nvEncGetEncodeProfileGUIDCount failed, error: %s", function_list->nvEncGetLastErrorString(nvenc_encoder)); goto fail; } @@ -116,12 +117,12 @@ static bool encoder_get_supported_profiles(const NV_ENCODE_API_FUNCTION_LIST *fu profile_guids = calloc(profile_guid_count, sizeof(GUID)); if(!profile_guids) { - fprintf(stderr, "gsr error: gsr_get_supported_video_codecs_nvenc: failed to allocate %d guids\n", (int)profile_guid_count); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_get_supported_video_codecs_nvenc: failed to allocate %d guids", (int)profile_guid_count); goto fail; } if(function_list->nvEncGetEncodeProfileGUIDs(nvenc_encoder, *encoder_guid, profile_guids, profile_guid_count, &profile_guid_count) != NV_ENC_SUCCESS) { - fprintf(stderr, "gsr error: gsr_get_supported_video_codecs_nvenc: nvEncGetEncodeProfileGUIDs failed, error: %s\n", function_list->nvEncGetLastErrorString(nvenc_encoder)); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_get_supported_video_codecs_nvenc: nvEncGetEncodeProfileGUIDs failed, error: %s", function_list->nvEncGetLastErrorString(nvenc_encoder)); goto fail; } @@ -157,7 +158,7 @@ static bool get_supported_video_codecs(const NV_ENCODE_API_FUNCTION_LIST *functi uint32_t encode_guid_count = 0; if(function_list->nvEncGetEncodeGUIDCount(nvenc_encoder, &encode_guid_count) != NV_ENC_SUCCESS) { - fprintf(stderr, "gsr error: gsr_get_supported_video_codecs_nvenc: nvEncGetEncodeGUIDCount failed, error: %s\n", function_list->nvEncGetLastErrorString(nvenc_encoder)); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_get_supported_video_codecs_nvenc: nvEncGetEncodeGUIDCount failed, error: %s", function_list->nvEncGetLastErrorString(nvenc_encoder)); goto fail; } @@ -166,12 +167,12 @@ static bool get_supported_video_codecs(const NV_ENCODE_API_FUNCTION_LIST *functi encoder_guids = calloc(encode_guid_count, sizeof(GUID)); if(!encoder_guids) { - fprintf(stderr, "gsr error: gsr_get_supported_video_codecs_nvenc: failed to allocate %d guids\n", (int)encode_guid_count); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_get_supported_video_codecs_nvenc: failed to allocate %d guids", (int)encode_guid_count); goto fail; } if(function_list->nvEncGetEncodeGUIDs(nvenc_encoder, encoder_guids, encode_guid_count, &encode_guid_count) != NV_ENC_SUCCESS) { - fprintf(stderr, "gsr error: gsr_get_supported_video_codecs_nvenc: nvEncGetEncodeGUIDs failed, error: %s\n", function_list->nvEncGetLastErrorString(nvenc_encoder)); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_get_supported_video_codecs_nvenc: nvEncGetEncodeGUIDs failed, error: %s", function_list->nvEncGetLastErrorString(nvenc_encoder)); goto fail; } @@ -198,7 +199,7 @@ bool gsr_get_supported_video_codecs_nvenc(gsr_supported_video_codecs *video_code memset(&cuda, 0, sizeof(cuda)); if(!gsr_cuda_load(&cuda)) { - fprintf(stderr, "gsr error: gsr_get_supported_video_codecs_nvenc: failed to load cuda\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_get_supported_video_codecs_nvenc: failed to load cuda"); goto done; } @@ -209,7 +210,7 @@ bool gsr_get_supported_video_codecs_nvenc(gsr_supported_video_codecs *video_code typedef NVENCSTATUS NVENCAPI (*FUNC_NvEncodeAPICreateInstance)(NV_ENCODE_API_FUNCTION_LIST *functionList); FUNC_NvEncodeAPICreateInstance nvEncodeAPICreateInstance = (FUNC_NvEncodeAPICreateInstance)dlsym(nvenc_lib, "NvEncodeAPICreateInstance"); if(!nvEncodeAPICreateInstance) { - fprintf(stderr, "gsr error: gsr_get_supported_video_codecs_nvenc: failed to find NvEncodeAPICreateInstance in libnvidia-encode.so\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_get_supported_video_codecs_nvenc: failed to find NvEncodeAPICreateInstance in libnvidia-encode.so"); goto done; } @@ -217,7 +218,7 @@ bool gsr_get_supported_video_codecs_nvenc(gsr_supported_video_codecs *video_code memset(&function_list, 0, sizeof(function_list)); function_list.version = NVENCAPI_STRUCT_VERSION(2); if(nvEncodeAPICreateInstance(&function_list) != NV_ENC_SUCCESS) { - fprintf(stderr, "gsr error: gsr_get_supported_video_codecs_nvenc: nvEncodeAPICreateInstance failed\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_get_supported_video_codecs_nvenc: nvEncodeAPICreateInstance failed"); goto done; } @@ -232,14 +233,14 @@ bool gsr_get_supported_video_codecs_nvenc(gsr_supported_video_codecs *video_code // In such cases fallback to old api version if possible and try again. function_list.version = NVENCAPI_STRUCT_VERSION_CUSTOM(NVENCAPI_VERSION_470, 2); if(nvEncodeAPICreateInstance(&function_list) != NV_ENC_SUCCESS) { - fprintf(stderr, "gsr error: gsr_get_supported_video_codecs_nvenc: nvEncodeAPICreateInstance (retry) failed\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_get_supported_video_codecs_nvenc: nvEncodeAPICreateInstance (retry) failed"); goto done; } params.version = NVENCAPI_STRUCT_VERSION_CUSTOM(NVENCAPI_VERSION_470, 1); params.apiVersion = NVENCAPI_VERSION_470; if(function_list.nvEncOpenEncodeSessionEx(¶ms, &nvenc_encoder) != NV_ENC_SUCCESS) { - fprintf(stderr, "gsr error: gsr_get_supported_video_codecs_nvenc: nvEncOpenEncodeSessionEx (retry) failed\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_get_supported_video_codecs_nvenc: nvEncOpenEncodeSessionEx (retry) failed"); goto done; } } diff --git a/src/codec_query/vaapi.c b/src/codec_query/vaapi.c index 0943450..4cc1bab 100644 --- a/src/codec_query/vaapi.c +++ b/src/codec_query/vaapi.c @@ -1,4 +1,5 @@ #include "../../include/codec_query/vaapi.h" +#include "../../include/log.h" #include "../../include/utils.h" #include @@ -134,7 +135,7 @@ static bool get_supported_video_codecs(VADisplay va_dpy, gsr_supported_video_cod int va_major = 0; int va_minor = 0; if(vaInitialize(va_dpy, &va_major, &va_minor) != VA_STATUS_SUCCESS) { - fprintf(stderr, "gsr error: gsr_get_supported_video_codecs_vaapi: vaInitialize failed\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_get_supported_video_codecs_vaapi: vaInitialize failed"); return false; } @@ -207,20 +208,20 @@ bool gsr_get_supported_video_codecs_vaapi(gsr_supported_video_codecs *video_code char render_path[128]; if(!gsr_card_path_get_render_path(card_path, render_path)) { - fprintf(stderr, "gsr error: gsr_get_supported_video_codecs_vaapi: failed to get /dev/dri/renderDXXX file from %s\n", card_path); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_get_supported_video_codecs_vaapi: failed to get /dev/dri/renderDXXX file from %s", card_path); goto done; } drm_fd = open(render_path, O_RDWR); if(drm_fd == -1) { - fprintf(stderr, "gsr error: gsr_get_supported_video_codecs_vaapi: failed to open device %s\n", render_path); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_get_supported_video_codecs_vaapi: failed to open device %s", render_path); goto done; } VADisplay va_dpy = vaGetDisplayDRM(drm_fd); if(va_dpy) { if(!get_supported_video_codecs(va_dpy, video_codecs, cleanup)) { - fprintf(stderr, "gsr error: gsr_get_supported_video_codecs_vaapi: failed to query supported video codecs for device %s\n", render_path); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_get_supported_video_codecs_vaapi: failed to query supported video codecs for device %s", render_path); goto done; } success = true; diff --git a/src/codec_query/vulkan.c b/src/codec_query/vulkan.c index 7dcb44e..d61cee5 100644 --- a/src/codec_query/vulkan.c +++ b/src/codec_query/vulkan.c @@ -1,4 +1,5 @@ #include "../../include/codec_query/vulkan.h" +#include "../../include/log.h" #include "../../include/utils.h" #include @@ -162,25 +163,25 @@ bool gsr_get_supported_video_codecs_vulkan(gsr_supported_video_codecs *video_cod char render_path[128]; if(!gsr_card_path_get_render_path(card_path, render_path)) { - fprintf(stderr, "gsr error: gsr_get_supported_video_codecs_vulkan: failed to get /dev/dri/renderDXXX file from %s\n", card_path); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_get_supported_video_codecs_vulkan: failed to get /dev/dri/renderDXXX file from %s", card_path); return false; } libvulkan = dlopen("libvulkan.so.1", RTLD_NOW); if (!libvulkan) { - fprintf(stderr, "gsr error: gsr_get_supported_video_codecs_vulkan: failed to load libvulkan.so.1, error: %s\n", dlerror()); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_get_supported_video_codecs_vulkan: failed to load libvulkan.so.1, error: %s", dlerror()); return false; } PFN_vkGetInstanceProcAddr vkGetInstanceProcAddr = (PFN_vkGetInstanceProcAddr)dlsym(libvulkan, "vkGetInstanceProcAddr"); if (!vkGetInstanceProcAddr) { - fprintf(stderr, "gsr error: gsr_get_supported_video_codecs_vulkan: could not find vkGetInstanceProcAddr in libvulkan.so.1\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_get_supported_video_codecs_vulkan: could not find vkGetInstanceProcAddr in libvulkan.so.1"); goto done; } PFN_vkCreateInstance vkCreateInstance = (PFN_vkCreateInstance)vkGetInstanceProcAddr(NULL, "vkCreateInstance"); if(!vkCreateInstance) { - fprintf(stderr, "gsr error: gsr_get_supported_video_codecs_vulkan: could not find vkCreateInstance in libvulkan.so.1\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_get_supported_video_codecs_vulkan: could not find vkCreateInstance in libvulkan.so.1"); goto done; } @@ -199,7 +200,7 @@ bool gsr_get_supported_video_codecs_vulkan(gsr_supported_video_codecs *video_cod }; if(vkCreateInstance(&instance_create_info, NULL, &instance) != VK_SUCCESS) { - fprintf(stderr, "gsr error: gsr_get_supported_video_codecs_vulkan: vkCreateInstance failed\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_get_supported_video_codecs_vulkan: vkCreateInstance failed"); goto done; } @@ -211,7 +212,7 @@ bool gsr_get_supported_video_codecs_vulkan(gsr_supported_video_codecs *video_cod PFN_vkDestroyDevice vkDestroyDevice = NULL; PFN_vkGetPhysicalDeviceVideoCapabilitiesKHR vkGetPhysicalDeviceVideoCapabilitiesKHR = NULL; - #define LOAD_INST(name) name = (PFN_##name)vkGetInstanceProcAddr(instance, #name); if(!name) { fprintf(stderr, "gsr error: gsr_get_supported_video_codecs_vulkan: could not find " #name " in libvulkan.so.1\n"); goto done; } + #define LOAD_INST(name) name = (PFN_##name)vkGetInstanceProcAddr(instance, #name); if(!name) { gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_get_supported_video_codecs_vulkan: could not find " #name " in libvulkan.so.1"); goto done; } LOAD_INST(vkEnumeratePhysicalDevices) LOAD_INST(vkDestroyInstance) @@ -225,12 +226,12 @@ bool gsr_get_supported_video_codecs_vulkan(gsr_supported_video_codecs *video_cod uint32_t num_devices = 0; if(vkEnumeratePhysicalDevices(instance, &num_devices, NULL) != VK_SUCCESS) { - fprintf(stderr, "gsr error: gsr_get_supported_video_codecs_vulkan: vkEnumeratePhysicalDevices (query num devices) failed\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_get_supported_video_codecs_vulkan: vkEnumeratePhysicalDevices (query num devices) failed"); goto done; } if(num_devices == 0) { - fprintf(stderr, "gsr error: gsr_get_supported_video_codecs_vulkan: no vulkan capable device found\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_get_supported_video_codecs_vulkan: no vulkan capable device found"); goto done; } @@ -238,7 +239,7 @@ bool gsr_get_supported_video_codecs_vulkan(gsr_supported_video_codecs *video_cod num_devices = MAX_PHYSICAL_DEVICES; if(vkEnumeratePhysicalDevices(instance, &num_devices, physical_devices) != VK_SUCCESS) { - fprintf(stderr, "gsr error: gsr_get_supported_video_codecs_vulkan: vkEnumeratePhysicalDevices (get data) failed\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_get_supported_video_codecs_vulkan: vkEnumeratePhysicalDevices (get data) failed"); goto done; } @@ -267,7 +268,7 @@ bool gsr_get_supported_video_codecs_vulkan(gsr_supported_video_codecs *video_cod } if(!physical_device) { - fprintf(stderr, "gsr error: gsr_get_supported_video_codecs_vulkan: failed to find a vulkan device that matches opengl device %s\n", card_path); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_get_supported_video_codecs_vulkan: failed to find a vulkan device that matches opengl device %s", card_path); goto done; } @@ -284,18 +285,18 @@ bool gsr_get_supported_video_codecs_vulkan(gsr_supported_video_codecs *video_cod uint32_t num_device_extensions = 0; if(vkEnumerateDeviceExtensionProperties(physical_device, NULL, &num_device_extensions, NULL) != VK_SUCCESS) { - fprintf(stderr, "gsr error: gsr_get_supported_video_codecs_vulkan: vkEnumerateDeviceExtensionProperties (query num device extensions) failed\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_get_supported_video_codecs_vulkan: vkEnumerateDeviceExtensionProperties (query num device extensions) failed"); goto done; } device_extensions = calloc(num_device_extensions, sizeof(VkExtensionProperties)); if(!device_extensions) { - fprintf(stderr, "gsr error: gsr_get_supported_video_codecs_vulkan: failed to allocate %d device extensions\n", num_device_extensions); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_get_supported_video_codecs_vulkan: failed to allocate %d device extensions", num_device_extensions); goto done; } if(vkEnumerateDeviceExtensionProperties(physical_device, NULL, &num_device_extensions, device_extensions) != VK_SUCCESS) { - fprintf(stderr, "gsr error: gsr_get_supported_video_codecs_vulkan: vkEnumerateDeviceExtensionProperties (get data) failed\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_get_supported_video_codecs_vulkan: vkEnumerateDeviceExtensionProperties (get data) failed"); goto done; } diff --git a/src/color_conversion.c b/src/color_conversion.c index 8076c2b..da922cd 100644 --- a/src/color_conversion.c +++ b/src/color_conversion.c @@ -1,4 +1,5 @@ #include "../include/color_conversion.h" +#include "../include/log.h" #include "../include/egl.h" #include #include @@ -620,23 +621,23 @@ static bool gsr_color_conversion_load_hdr_graphics_shaders(gsr_color_conversion case GSR_DESTINATION_COLOR_NV12: case GSR_DESTINATION_COLOR_P010: { if(load_graphics_shader_y(&self->graphics_shaders[GRAPHICS_SHADER_INDEX_Y_HDR], self->params.egl, &self->graphics_uniforms[GRAPHICS_SHADER_INDEX_Y_HDR], self->params.destination_color, self->params.color_range, false, true) != 0) { - fprintf(stderr, "gsr error: gsr_color_conversion_load_hdr_graphics_shaders: failed to load Y graphics shader (hdr)\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_color_conversion_load_hdr_graphics_shaders: failed to load Y graphics shader (hdr)"); return false; } if(load_graphics_shader_uv(&self->graphics_shaders[GRAPHICS_SHADER_INDEX_UV_HDR], self->params.egl, &self->graphics_uniforms[GRAPHICS_SHADER_INDEX_UV_HDR], self->params.destination_color, self->params.color_range, false, true) != 0) { - fprintf(stderr, "gsr error: gsr_color_conversion_load_hdr_graphics_shaders: failed to load UV graphics shader (hdr)\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_color_conversion_load_hdr_graphics_shaders: failed to load UV graphics shader (hdr)"); return false; } if(self->params.load_external_image_shader) { if(load_graphics_shader_y(&self->graphics_shaders[GRAPHICS_SHADER_INDEX_Y_HDR_EXTERNAL], self->params.egl, &self->graphics_uniforms[GRAPHICS_SHADER_INDEX_Y_HDR_EXTERNAL], self->params.destination_color, self->params.color_range, true, true) != 0) { - fprintf(stderr, "gsr error: gsr_color_conversion_load_hdr_graphics_shaders: failed to load Y graphics shader (hdr, external)\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_color_conversion_load_hdr_graphics_shaders: failed to load Y graphics shader (hdr, external)"); return false; } if(load_graphics_shader_uv(&self->graphics_shaders[GRAPHICS_SHADER_INDEX_UV_HDR_EXTERNAL], self->params.egl, &self->graphics_uniforms[GRAPHICS_SHADER_INDEX_UV_HDR_EXTERNAL], self->params.destination_color, self->params.color_range, true, true) != 0) { - fprintf(stderr, "gsr error: gsr_color_conversion_load_hdr_graphics_shaders: failed to load UV graphics shader (hdr, external)\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_color_conversion_load_hdr_graphics_shaders: failed to load UV graphics shader (hdr, external)"); return false; } } @@ -644,13 +645,13 @@ static bool gsr_color_conversion_load_hdr_graphics_shaders(gsr_color_conversion } case GSR_DESTINATION_COLOR_RGB: { if(load_graphics_shader_rgb(&self->graphics_shaders[GRAPHICS_SHADER_INDEX_RGB_HDR], self->params.egl, &self->graphics_uniforms[GRAPHICS_SHADER_INDEX_RGB_HDR], false, true) != 0) { - fprintf(stderr, "gsr error: gsr_color_conversion_load_hdr_graphics_shaders: failed to load RGB graphics shader (hdr)\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_color_conversion_load_hdr_graphics_shaders: failed to load RGB graphics shader (hdr)"); return false; } if(self->params.load_external_image_shader) { if(load_graphics_shader_rgb(&self->graphics_shaders[GRAPHICS_SHADER_INDEX_RGB_HDR_EXTERNAL], self->params.egl, &self->graphics_uniforms[GRAPHICS_SHADER_INDEX_RGB_HDR_EXTERNAL], true, true) != 0) { - fprintf(stderr, "gsr error: gsr_color_conversion_load_hdr_graphics_shaders: failed to load RGB graphics shader (hdr, external)\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_color_conversion_load_hdr_graphics_shaders: failed to load RGB graphics shader (hdr, external)"); return false; } } @@ -686,7 +687,7 @@ static int load_framebuffers(gsr_color_conversion *self) { self->params.egl->glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, self->params.destination_textures[0], 0); self->params.egl->glDrawBuffers(1, &draw_buffer); if(self->params.egl->glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) { - fprintf(stderr, "gsr error: gsr_color_conversion_init: failed to create framebuffer for Y\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_color_conversion_init: failed to create framebuffer for Y"); goto err; } @@ -695,7 +696,7 @@ static int load_framebuffers(gsr_color_conversion *self) { self->params.egl->glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, self->params.destination_textures[1], 0); self->params.egl->glDrawBuffers(1, &draw_buffer); if(self->params.egl->glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) { - fprintf(stderr, "gsr error: gsr_color_conversion_init: failed to create framebuffer for UV\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_color_conversion_init: failed to create framebuffer for UV"); goto err; } } @@ -731,34 +732,34 @@ static bool gsr_color_conversion_load_graphics_shaders(gsr_color_conversion *sel case GSR_DESTINATION_COLOR_NV12: case GSR_DESTINATION_COLOR_P010: { if(load_graphics_shader_y(&self->graphics_shaders[GRAPHICS_SHADER_INDEX_Y], self->params.egl, &self->graphics_uniforms[GRAPHICS_SHADER_INDEX_Y], self->params.destination_color, self->params.color_range, false, false) != 0) { - fprintf(stderr, "gsr error: gsr_color_conversion_init: failed to load Y graphics shader\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_color_conversion_init: failed to load Y graphics shader"); return false; } if(load_graphics_shader_uv(&self->graphics_shaders[GRAPHICS_SHADER_INDEX_UV], self->params.egl, &self->graphics_uniforms[GRAPHICS_SHADER_INDEX_UV], self->params.destination_color, self->params.color_range, false, false) != 0) { - fprintf(stderr, "gsr error: gsr_color_conversion_init: failed to load UV graphics shader\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_color_conversion_init: failed to load UV graphics shader"); return false; } if(load_graphics_shader_yuyv_to_y(&self->graphics_shaders[GRAPHICS_SHADER_INDEX_YUYV_TO_Y], self->params.egl, &self->graphics_uniforms[GRAPHICS_SHADER_INDEX_YUYV_TO_Y], false) != 0) { - fprintf(stderr, "gsr error: gsr_color_conversion_init: failed to load YUYV to Y graphics shader\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_color_conversion_init: failed to load YUYV to Y graphics shader"); return false; } if(load_graphics_shader_yuyv_to_uv(&self->graphics_shaders[GRAPHICS_SHADER_INDEX_YUYV_TO_UV], self->params.egl, &self->graphics_uniforms[GRAPHICS_SHADER_INDEX_YUYV_TO_UV], false) != 0) { - fprintf(stderr, "gsr error: gsr_color_conversion_init: failed to load YUYV to UV graphics shader\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_color_conversion_init: failed to load YUYV to UV graphics shader"); return false; } break; } case GSR_DESTINATION_COLOR_RGB: { if(load_graphics_shader_rgb(&self->graphics_shaders[GRAPHICS_SHADER_INDEX_RGB], self->params.egl, &self->graphics_uniforms[GRAPHICS_SHADER_INDEX_RGB], false, false) != 0) { - fprintf(stderr, "gsr error: gsr_color_conversion_init: failed to load RGB graphics shader\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_color_conversion_init: failed to load RGB graphics shader"); return false; } if(load_graphics_shader_yuyv_to_rgb(&self->graphics_shaders[GRAPHICS_SHADER_INDEX_YUYV_TO_RGB], self->params.egl, &self->graphics_uniforms[GRAPHICS_SHADER_INDEX_YUYV_TO_RGB], false) != 0) { - fprintf(stderr, "gsr error: gsr_color_conversion_init: failed to load YUYV to RGB graphics shader\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_color_conversion_init: failed to load YUYV to RGB graphics shader"); return false; } break; @@ -772,34 +773,34 @@ static bool gsr_color_conversion_load_external_graphics_shaders(gsr_color_conver case GSR_DESTINATION_COLOR_NV12: case GSR_DESTINATION_COLOR_P010: { if(load_graphics_shader_y(&self->graphics_shaders[GRAPHICS_SHADER_INDEX_Y_EXTERNAL], self->params.egl, &self->graphics_uniforms[GRAPHICS_SHADER_INDEX_Y_EXTERNAL], self->params.destination_color, self->params.color_range, true, false) != 0) { - fprintf(stderr, "gsr error: gsr_color_conversion_init: failed to load Y graphics shader (external)\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_color_conversion_init: failed to load Y graphics shader (external)"); return false; } if(load_graphics_shader_uv(&self->graphics_shaders[GRAPHICS_SHADER_INDEX_UV_EXTERNAL], self->params.egl, &self->graphics_uniforms[GRAPHICS_SHADER_INDEX_UV_EXTERNAL], self->params.destination_color, self->params.color_range, true, false) != 0) { - fprintf(stderr, "gsr error: gsr_color_conversion_init: failed to load UV graphics shader (external)\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_color_conversion_init: failed to load UV graphics shader (external)"); return false; } if(load_graphics_shader_yuyv_to_y(&self->graphics_shaders[GRAPHICS_SHADER_INDEX_YUYV_TO_Y_EXTERNAL], self->params.egl, &self->graphics_uniforms[GRAPHICS_SHADER_INDEX_YUYV_TO_Y_EXTERNAL], true) != 0) { - fprintf(stderr, "gsr error: gsr_color_conversion_init: failed to load YUYV to Y graphics shader (external)\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_color_conversion_init: failed to load YUYV to Y graphics shader (external)"); return false; } if(load_graphics_shader_yuyv_to_uv(&self->graphics_shaders[GRAPHICS_SHADER_INDEX_YUYV_TO_UV_EXTERNAL], self->params.egl, &self->graphics_uniforms[GRAPHICS_SHADER_INDEX_YUYV_TO_UV_EXTERNAL], true) != 0) { - fprintf(stderr, "gsr error: gsr_color_conversion_init: failed to load YUYV to UV graphics shader (external)\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_color_conversion_init: failed to load YUYV to UV graphics shader (external)"); return false; } break; } case GSR_DESTINATION_COLOR_RGB: { if(load_graphics_shader_rgb(&self->graphics_shaders[GRAPHICS_SHADER_INDEX_RGB_EXTERNAL], self->params.egl, &self->graphics_uniforms[GRAPHICS_SHADER_INDEX_RGB_EXTERNAL], true, false) != 0) { - fprintf(stderr, "gsr error: gsr_color_conversion_init: failed to load RGB graphics shader (external)\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_color_conversion_init: failed to load RGB graphics shader (external)"); return false; } if(load_graphics_shader_yuyv_to_rgb(&self->graphics_shaders[GRAPHICS_SHADER_INDEX_YUYV_TO_RGB_EXTERNAL], self->params.egl, &self->graphics_uniforms[GRAPHICS_SHADER_INDEX_YUYV_TO_RGB_EXTERNAL], true) != 0) { - fprintf(stderr, "gsr error: gsr_color_conversion_init: failed to load YUYV to RGB graphics shader (external)\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_color_conversion_init: failed to load YUYV to RGB graphics shader (external)"); return false; } break; @@ -819,14 +820,14 @@ int gsr_color_conversion_init(gsr_color_conversion *self, const gsr_color_conver case GSR_DESTINATION_COLOR_NV12: case GSR_DESTINATION_COLOR_P010: { if(self->params.num_destination_textures != 2) { - fprintf(stderr, "gsr error: gsr_color_conversion_init: expected 2 destination textures for destination color NV12/P010, got %d destination texture(s)\n", self->params.num_destination_textures); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_color_conversion_init: expected 2 destination textures for destination color NV12/P010, got %d destination texture(s)", self->params.num_destination_textures); goto err; } break; } case GSR_DESTINATION_COLOR_RGB: { if(self->params.num_destination_textures != 1) { - fprintf(stderr, "gsr error: gsr_color_conversion_init: expected 1 destination textures for destination color RGB8, got %d destination texture(s)\n", self->params.num_destination_textures); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_color_conversion_init: expected 1 destination textures for destination color RGB8, got %d destination texture(s)", self->params.num_destination_textures); goto err; } break; @@ -1143,7 +1144,7 @@ static void gsr_color_conversion_draw_graphics(gsr_color_conversion *self, unsig void gsr_color_conversion_draw(gsr_color_conversion *self, unsigned int texture_id, vec2i destination_pos, vec2i destination_size, vec2i source_pos, vec2i source_size, vec2i texture_size, gsr_rotation rotation, gsr_flip flip, gsr_source_color source_color, bool external_texture) { assert(!external_texture || self->params.load_external_image_shader); if(external_texture && !self->params.load_external_image_shader) { - fprintf(stderr, "gsr error: gsr_color_conversion_draw: external texture not loaded\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_color_conversion_draw: external texture not loaded"); return; } diff --git a/src/cuda.c b/src/cuda.c index bfafbcd..55d1f40 100644 --- a/src/cuda.c +++ b/src/cuda.c @@ -1,4 +1,5 @@ #include "../include/cuda.h" +#include "../include/log.h" #include "../include/library_loader.h" #include #include @@ -13,7 +14,7 @@ bool gsr_cuda_load(gsr_cuda *self) { if(!lib) { lib = dlopen("libcuda.so", RTLD_LAZY); if(!lib) { - fprintf(stderr, "gsr error: gsr_cuda_load failed: failed to load libcuda.so/libcuda.so.1, error: %s\n", dlerror()); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_cuda_load failed: failed to load libcuda.so/libcuda.so.1, error: %s", dlerror()); return false; } } @@ -45,7 +46,7 @@ bool gsr_cuda_load(gsr_cuda *self) { CUresult res; if(!dlsym_load_list(lib, required_dlsym)) { - fprintf(stderr, "gsr error: gsr_cuda_load failed: missing required symbols in libcuda.so/libcuda.so.1\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_cuda_load failed: missing required symbols in libcuda.so/libcuda.so.1"); goto fail; } @@ -53,14 +54,14 @@ bool gsr_cuda_load(gsr_cuda *self) { if(res != CUDA_SUCCESS) { const char *err_str = "unknown"; self->cuGetErrorString(res, &err_str); - fprintf(stderr, "gsr error: gsr_cuda_load failed: cuInit failed, error: %s (result: %d)\n", err_str, res); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_cuda_load failed: cuInit failed, error: %s (result: %d)", err_str, res); goto fail; } int nGpu = 0; self->cuDeviceGetCount(&nGpu); if(nGpu <= 0) { - fprintf(stderr, "gsr error: gsr_cuda_load failed: no cuda supported devices found\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_cuda_load failed: no cuda supported devices found"); goto fail; } @@ -70,7 +71,7 @@ bool gsr_cuda_load(gsr_cuda *self) { if(res != CUDA_SUCCESS) { const char *err_str = "unknown"; self->cuGetErrorString(res, &err_str); - fprintf(stderr, "gsr error: gsr_cuda_load failed: unable to get CUDA device, error: %s (result: %d)\n", err_str, res); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_cuda_load failed: unable to get CUDA device, error: %s (result: %d)", err_str, res); goto fail; } @@ -78,7 +79,7 @@ bool gsr_cuda_load(gsr_cuda *self) { if(res != CUDA_SUCCESS) { const char *err_str = "unknown"; self->cuGetErrorString(res, &err_str); - fprintf(stderr, "gsr error: gsr_cuda_load failed: unable to create CUDA context, error: %s (result: %d)\n", err_str, res); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_cuda_load failed: unable to create CUDA context, error: %s (result: %d)", err_str, res); goto fail; } diff --git a/src/cursor.c b/src/cursor.c index 19047ba..65d9d4c 100644 --- a/src/cursor.c +++ b/src/cursor.c @@ -1,4 +1,5 @@ #include "../include/cursor.h" +#include "../include/log.h" #include #include @@ -81,7 +82,7 @@ int gsr_cursor_init(gsr_cursor *self, gsr_egl *egl, Display *display) { self->x_fixes_event_base = 0; if(!XFixesQueryExtension(self->display, &self->x_fixes_event_base, &x_fixes_error_base)) { - fprintf(stderr, "gsr error: gsr_cursor_init: your X11 server is missing the XFixes extension\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_cursor_init: your X11 server is missing the XFixes extension"); gsr_cursor_deinit(self); return -1; } diff --git a/src/damage.c b/src/damage.c index bbc025a..351f09a 100644 --- a/src/damage.c +++ b/src/damage.c @@ -1,4 +1,5 @@ #include "../include/damage.h" +#include "../include/log.h" #include "../include/utils.h" #include "../include/window/window.h" @@ -46,13 +47,13 @@ static void add_monitor_callback(const gsr_monitor *monitor, void *userdata) { } if(self->num_monitors + 1 > GSR_DAMAGE_MAX_MONITORS) { - fprintf(stderr, "gsr error: gsr_damage_on_output_change: max monitors reached\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_damage_on_output_change: max monitors reached"); return; } char *monitor_name_copy = strdup(monitor->name); if(!monitor_name_copy) { - fprintf(stderr, "gsr error: gsr_damage_on_output_change: strdup failed for monitor: %s\n", monitor->name); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_damage_on_output_change: strdup failed for monitor: %s", monitor->name); return; } @@ -71,25 +72,25 @@ bool gsr_damage_init(gsr_damage *self, gsr_egl *egl, gsr_cursor *cursor, bool tr self->cursor = cursor; if(gsr_window_get_display_server(egl->window) != GSR_DISPLAY_SERVER_X11) { - fprintf(stderr, "gsr error: gsr_damage_init: damage tracking is not supported on wayland\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_damage_init: damage tracking is not supported on wayland"); return false; } self->display = gsr_window_get_display(egl->window); if(!XDamageQueryExtension(self->display, &self->damage_event, &self->damage_error)) { - fprintf(stderr, "gsr error: gsr_damage_init: XDamage is not supported by your X11 server\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_damage_init: XDamage is not supported by your X11 server"); gsr_damage_deinit(self); return false; } if(!XRRQueryExtension(self->display, &self->randr_event, &self->randr_error)) { - fprintf(stderr, "gsr error: gsr_damage_init: XRandr is not supported by your X11 server\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_damage_init: XRandr is not supported by your X11 server"); gsr_damage_deinit(self); return false; } if(!xrandr_is_supported(self->display)) { - fprintf(stderr, "gsr error: gsr_damage_init: your X11 randr version is too old\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_damage_init: your X11 randr version is too old"); gsr_damage_deinit(self); return false; } @@ -98,7 +99,7 @@ bool gsr_damage_init(gsr_damage *self, gsr_egl *egl, gsr_cursor *cursor, bool tr self->monitor_damage = XDamageCreate(self->display, DefaultRootWindow(self->display), XDamageReportNonEmpty); if(!self->monitor_damage) { - fprintf(stderr, "gsr error: gsr_damage_init: XDamageCreate failed\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_damage_init: XDamageCreate failed"); gsr_damage_deinit(self); return false; } @@ -163,7 +164,7 @@ bool gsr_damage_start_tracking_window(gsr_damage *self, int64_t window) { } if(self->num_windows_tracked + 1 > GSR_DAMAGE_MAX_TRACKED_TARGETS) { - fprintf(stderr, "gsr error: gsr_damage_start_tracking_window: max window targets reached\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_damage_start_tracking_window: max window targets reached"); return false; } @@ -173,11 +174,11 @@ bool gsr_damage_start_tracking_window(gsr_damage *self, int64_t window) { win_attr.width = 0; win_attr.height = 0; if(!XGetWindowAttributes(self->display, window, &win_attr)) - fprintf(stderr, "gsr warning: gsr_damage_start_tracking_window failed: failed to get window attributes: %ld\n", (long)window); + gsr_log(GSR_LOG_LEVEL_WARNING, "gsr_damage_start_tracking_window failed: failed to get window attributes: %ld", (long)window); const Damage damage = XDamageCreate(self->display, window, XDamageReportNonEmpty); if(!damage) { - fprintf(stderr, "gsr error: gsr_damage_start_tracking_window: XDamageCreate failed\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_damage_start_tracking_window: XDamageCreate failed"); return false; } XDamageSubtract(self->display, damage, None, None); @@ -250,19 +251,19 @@ bool gsr_damage_start_tracking_monitor(gsr_damage *self, const char *monitor_nam } if(self->num_monitors_tracked + 1 > GSR_DAMAGE_MAX_TRACKED_TARGETS) { - fprintf(stderr, "gsr error: gsr_damage_start_tracking_monitor: max monitor targets reached\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_damage_start_tracking_monitor: max monitor targets reached"); return false; } char *monitor_name_copy = strdup(monitor_name); if(!monitor_name_copy) { - fprintf(stderr, "gsr error: gsr_damage_start_tracking_monitor: strdup failed for monitor: %s\n", monitor_name); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_damage_start_tracking_monitor: strdup failed for monitor: %s", monitor_name); return false; } gsr_monitor *monitor = gsr_damage_get_monitor_by_name(self, monitor_name); if(!monitor) { - fprintf(stderr, "gsr error: gsr_damage_start_tracking_monitor: failed to find monitor: %s\n", monitor_name); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_damage_start_tracking_monitor: failed to find monitor: %s", monitor_name); free(monitor_name_copy); return false; } diff --git a/src/dbus.c b/src/dbus.c index 715a021..bde9950 100644 --- a/src/dbus.c +++ b/src/dbus.c @@ -1,4 +1,5 @@ #include "../include/dbus.h" +#include "../include/log.h" #include @@ -32,7 +33,7 @@ typedef struct { static bool generate_random_characters(char *buffer, int buffer_size, const char *alphabet, size_t alphabet_size) { /* TODO: Use other functions on other platforms than linux */ if(getrandom(buffer, buffer_size, 0) < buffer_size) { - fprintf(stderr, "Failed to get random bytes, error: %s\n", strerror(errno)); + gsr_log(GSR_LOG_LEVEL_ERROR, "failed to get random bytes, error: %s", strerror(errno)); return false; } @@ -63,25 +64,25 @@ bool gsr_dbus_init(gsr_dbus *self, const char *screencast_restore_token) { self->random_str[DBUS_RANDOM_STR_SIZE] = '\0'; if(!generate_random_characters_standard_alphabet(self->random_str, DBUS_RANDOM_STR_SIZE)) { - fprintf(stderr, "gsr error: gsr_dbus_init: failed to generate random string\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_dbus_init: failed to generate random string"); return false; } self->con = dbus_bus_get(DBUS_BUS_SESSION, &self->err); if(dbus_error_is_set(&self->err)) { - fprintf(stderr, "gsr error: gsr_dbus_init: dbus_bus_get failed with error: %s\n", self->err.message); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_dbus_init: dbus_bus_get failed with error: %s", self->err.message); return false; } if(!self->con) { - fprintf(stderr, "gsr error: gsr_dbus_init: failed to get dbus session\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_dbus_init: failed to get dbus session"); return false; } if(screencast_restore_token) { self->screencast_restore_token = strdup(screencast_restore_token); if(!self->screencast_restore_token) { - fprintf(stderr, "gsr error: gsr_dbus_init: failed to clone restore token\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_dbus_init: failed to clone restore token"); gsr_dbus_deinit(self); return false; } @@ -121,7 +122,7 @@ static bool gsr_dbus_desktop_portal_get_property(gsr_dbus *self, const char *int "org.freedesktop.DBus.Properties", // interface to call on "Get"); // method name if(!msg) { - fprintf(stderr, "gsr error: gsr_dbus_desktop_portal_get_property: dbus_message_new_method_call failed\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_dbus_desktop_portal_get_property: dbus_message_new_method_call failed"); return false; } @@ -129,20 +130,20 @@ static bool gsr_dbus_desktop_portal_get_property(gsr_dbus *self, const char *int dbus_message_iter_init_append(msg, &it); if(!dbus_message_iter_append_basic(&it, DBUS_TYPE_STRING, &interface)) { - fprintf(stderr, "gsr error: gsr_dbus_desktop_portal_get_property: failed to add interface\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_dbus_desktop_portal_get_property: failed to add interface"); dbus_message_unref(msg); return false; } if(!dbus_message_iter_append_basic(&it, DBUS_TYPE_STRING, &property_name)) { - fprintf(stderr, "gsr error: gsr_dbus_desktop_portal_get_property: failed to add property_name\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_dbus_desktop_portal_get_property: failed to add property_name"); dbus_message_unref(msg); return false; } DBusPendingCall *pending = NULL; if(!dbus_connection_send_with_reply(self->con, msg, &pending, -1) || !pending) { // -1 is default timeout - fprintf(stderr, "gsr error: gsr_dbus_desktop_portal_get_property: dbus_connection_send_with_reply failed\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_dbus_desktop_portal_get_property: dbus_connection_send_with_reply failed"); dbus_message_unref(msg); return false; } @@ -157,7 +158,7 @@ static bool gsr_dbus_desktop_portal_get_property(gsr_dbus *self, const char *int msg = dbus_pending_call_steal_reply(pending); if(!msg) { - fprintf(stderr, "gsr error: gsr_dbus_desktop_portal_get_property: dbus_pending_call_steal_reply failed\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_dbus_desktop_portal_get_property: dbus_pending_call_steal_reply failed"); dbus_pending_call_unref(pending); dbus_message_unref(msg); return false; @@ -168,7 +169,7 @@ static bool gsr_dbus_desktop_portal_get_property(gsr_dbus *self, const char *int DBusMessageIter resp_args; if(!dbus_message_iter_init(msg, &resp_args)) { - fprintf(stderr, "gsr error: gsr_dbus_desktop_portal_get_property: response message is missing arguments\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_dbus_desktop_portal_get_property: response message is missing arguments"); dbus_message_unref(msg); return false; } else if(DBUS_TYPE_UINT32 == dbus_message_iter_get_arg_type(&resp_args)) { @@ -180,12 +181,12 @@ static bool gsr_dbus_desktop_portal_get_property(gsr_dbus *self, const char *int if(dbus_message_iter_get_arg_type(&variant_iter) == DBUS_TYPE_UINT32) { dbus_message_iter_get_basic(&variant_iter, result); } else { - fprintf(stderr, "gsr error: gsr_dbus_desktop_portal_get_property: response message is not a variant with an uint32, %c\n", dbus_message_iter_get_arg_type(&variant_iter)); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_dbus_desktop_portal_get_property: response message is not a variant with an uint32, %c", dbus_message_iter_get_arg_type(&variant_iter)); dbus_message_unref(msg); return false; } } else { - fprintf(stderr, "gsr error: gsr_dbus_desktop_portal_get_property: response message is not an uint32, %c\n", dbus_message_iter_get_arg_type(&resp_args)); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_dbus_desktop_portal_get_property: response message is not an uint32, %c", dbus_message_iter_get_arg_type(&resp_args)); dbus_message_unref(msg); return false; // TODO: Check dbus_error_is_set? @@ -208,7 +209,7 @@ static bool gsr_dbus_ensure_desktop_portal_rule_added(gsr_dbus *self) { dbus_bus_add_match(self->con, DESKTOP_PORTAL_SIGNAL_RULE, &self->err); dbus_connection_flush(self->con); if(dbus_error_is_set(&self->err)) { - fprintf(stderr, "gsr error: gsr_dbus_ensure_desktop_portal_rule_added: failed to add dbus rule %s, error: %s\n", DESKTOP_PORTAL_SIGNAL_RULE, self->err.message); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_dbus_ensure_desktop_portal_rule_added: failed to add dbus rule %s, error: %s", DESKTOP_PORTAL_SIGNAL_RULE, self->err.message); return false; } self->desktop_portal_rule_added = true; @@ -293,7 +294,7 @@ static bool gsr_dbus_call_screencast_method(gsr_dbus *self, const char *method_n "org.freedesktop.portal.ScreenCast", // interface to call on method_name); // method name if(!msg) { - fprintf(stderr, "gsr error: gsr_dbus_call_screencast_method: dbus_message_new_method_call failed\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_dbus_call_screencast_method: dbus_message_new_method_call failed"); return false; } @@ -302,7 +303,7 @@ static bool gsr_dbus_call_screencast_method(gsr_dbus *self, const char *method_n if(session_handle) { if(!dbus_message_iter_append_basic(&it, DBUS_TYPE_OBJECT_PATH, &session_handle)) { - fprintf(stderr, "gsr error: gsr_dbus_call_screencast_method: failed to add session_handle\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_dbus_call_screencast_method: failed to add session_handle"); dbus_message_unref(msg); return false; } @@ -310,21 +311,21 @@ static bool gsr_dbus_call_screencast_method(gsr_dbus *self, const char *method_n if(parent_window) { if(!dbus_message_iter_append_basic(&it, DBUS_TYPE_STRING, &parent_window)) { - fprintf(stderr, "gsr error: gsr_dbus_call_screencast_method: failed to add parent_window\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_dbus_call_screencast_method: failed to add parent_window"); dbus_message_unref(msg); return false; } } if(!dbus_add_dict(&it, entries, num_entries)) { - fprintf(stderr, "gsr error: gsr_dbus_call_screencast_method: failed to add dict\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_dbus_call_screencast_method: failed to add dict"); dbus_message_unref(msg); return false; } DBusPendingCall *pending = NULL; if(!dbus_connection_send_with_reply(self->con, msg, &pending, -1) || !pending) { // -1 is default timeout - fprintf(stderr, "gsr error: gsr_dbus_call_screencast_method: dbus_connection_send_with_reply failed\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_dbus_call_screencast_method: dbus_connection_send_with_reply failed"); dbus_message_unref(msg); return false; } @@ -339,7 +340,7 @@ static bool gsr_dbus_call_screencast_method(gsr_dbus *self, const char *method_n msg = dbus_pending_call_steal_reply(pending); if(!msg) { - fprintf(stderr, "gsr error: gsr_dbus_call_screencast_method: dbus_pending_call_steal_reply failed\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_dbus_call_screencast_method: dbus_pending_call_steal_reply failed"); dbus_pending_call_unref(pending); dbus_message_unref(msg); return false; @@ -350,7 +351,7 @@ static bool gsr_dbus_call_screencast_method(gsr_dbus *self, const char *method_n DBusMessageIter resp_args; if(!dbus_message_iter_init(msg, &resp_args)) { - fprintf(stderr, "gsr error: gsr_dbus_call_screencast_method: response message is missing arguments\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_dbus_call_screencast_method: response message is missing arguments"); dbus_message_unref(msg); return false; } else if (DBUS_TYPE_OBJECT_PATH == dbus_message_iter_get_arg_type(&resp_args)) { @@ -365,13 +366,13 @@ static bool gsr_dbus_call_screencast_method(gsr_dbus *self, const char *method_n } else if(DBUS_TYPE_STRING == dbus_message_iter_get_arg_type(&resp_args)) { char *err = NULL; dbus_message_iter_get_basic(&resp_args, &err); - fprintf(stderr, "gsr error: gsr_dbus_call_screencast_method: failed with error: %s\n", err); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_dbus_call_screencast_method: failed with error: %s", err); dbus_message_unref(msg); return false; // TODO: Check dbus_error_is_set? } else { - fprintf(stderr, "gsr error: gsr_dbus_call_screencast_method: response message is not an object path or unix fd\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_dbus_call_screencast_method: response message is not an object path or unix fd"); dbus_message_unref(msg); return false; // TODO: Check dbus_error_is_set? @@ -404,7 +405,7 @@ static bool gsr_dbus_call_screencast_method(gsr_dbus *self, const char *method_n static int gsr_dbus_get_response_status(DBusMessageIter *resp_args) { if(dbus_message_iter_get_arg_type(resp_args) != DBUS_TYPE_UINT32) { - fprintf(stderr, "gsr error: gsr_dbus_get_response_status: missing uint32 in response\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_dbus_get_response_status: missing uint32 in response"); return -1; } @@ -425,7 +426,7 @@ static dict_entry* find_dict_entry_by_key(dict_entry *entries, int num_entries, static bool gsr_dbus_get_variant_value(DBusMessageIter *iter, dict_entry *entry) { if(dbus_message_iter_get_arg_type(iter) != DBUS_TYPE_VARIANT) { - fprintf(stderr, "gsr error: gsr_dbus_get_variant_value: value is not a variant\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_dbus_get_variant_value: value is not a variant"); return false; } @@ -435,7 +436,7 @@ static bool gsr_dbus_get_variant_value(DBusMessageIter *iter, dict_entry *entry) switch(dbus_message_iter_get_arg_type(&variant_iter)) { case DBUS_TYPE_STRING: { if(entry->value_type != DICT_TYPE_STRING) { - fprintf(stderr, "gsr error: gsr_dbus_get_variant_value: expected entry value to be a(n) %s was a string\n", dict_value_type_to_string(entry->value_type)); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_dbus_get_variant_value: expected entry value to be a(n) %s was a string", dict_value_type_to_string(entry->value_type)); return false; } @@ -443,7 +444,7 @@ static bool gsr_dbus_get_variant_value(DBusMessageIter *iter, dict_entry *entry) dbus_message_iter_get_basic(&variant_iter, &value); if(!value) { - fprintf(stderr, "gsr error: gsr_dbus_get_variant_value: failed to get entry value as value\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_dbus_get_variant_value: failed to get entry value as value"); return false; } @@ -454,14 +455,14 @@ static bool gsr_dbus_get_variant_value(DBusMessageIter *iter, dict_entry *entry) entry->str = strdup(value); if(!entry->str) { - fprintf(stderr, "gsr error: gsr_dbus_get_variant_value: failed to copy value\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_dbus_get_variant_value: failed to copy value"); return false; } return true; } case DBUS_TYPE_UINT32: { if(entry->value_type != DICT_TYPE_UINT32) { - fprintf(stderr, "gsr error: gsr_dbus_get_variant_value: expected entry value to be a(n) %s was an uint32\n", dict_value_type_to_string(entry->value_type)); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_dbus_get_variant_value: expected entry value to be a(n) %s was an uint32", dict_value_type_to_string(entry->value_type)); return false; } @@ -470,7 +471,7 @@ static bool gsr_dbus_get_variant_value(DBusMessageIter *iter, dict_entry *entry) } case DBUS_TYPE_BOOLEAN: { if(entry->value_type != DICT_TYPE_BOOL) { - fprintf(stderr, "gsr error: gsr_dbus_get_variant_value: expected entry value to be a(n) %s was a boolean\n", dict_value_type_to_string(entry->value_type)); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_dbus_get_variant_value: expected entry value to be a(n) %s was a boolean", dict_value_type_to_string(entry->value_type)); return false; } @@ -479,7 +480,7 @@ static bool gsr_dbus_get_variant_value(DBusMessageIter *iter, dict_entry *entry) } } - fprintf(stderr, "gsr error: gsr_dbus_get_variant_value: got unexpected type, expected string, uint32 or boolean\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_dbus_get_variant_value: got unexpected type, expected string, uint32 or boolean"); return false; } @@ -492,7 +493,7 @@ static bool gsr_dbus_get_variant_value(DBusMessageIter *iter, dict_entry *entry) */ static bool gsr_dbus_get_map(DBusMessageIter *resp_args, dict_entry *entries, int num_entries) { if(dbus_message_iter_get_arg_type(resp_args) != DBUS_TYPE_ARRAY) { - fprintf(stderr, "gsr error: gsr_dbus_get_map: missing array in response\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_dbus_get_map: missing array in response"); return false; } @@ -508,20 +509,20 @@ static bool gsr_dbus_get_map(DBusMessageIter *resp_args, dict_entry *entries, in // dbus_message_iter_get_arg_type(&subiter), // dbus_message_iter_get_signature(&subiter)); if(dbus_message_iter_get_arg_type(&subiter) != DBUS_TYPE_DICT_ENTRY) { - fprintf(stderr, "gsr error: gsr_dbus_get_map: array value is not an entry\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_dbus_get_map: array value is not an entry"); return false; } dbus_message_iter_recurse(&subiter, &dictiter); if(dbus_message_iter_get_arg_type(&dictiter) != DBUS_TYPE_STRING) { - fprintf(stderr, "gsr error: gsr_dbus_get_map: entry key is not a string\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_dbus_get_map: entry key is not a string"); goto error; } dbus_message_iter_get_basic(&dictiter, &key); if(!key) { - fprintf(stderr, "gsr error: gsr_dbus_get_map: failed to get entry key as value\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_dbus_get_map: failed to get entry key as value"); goto error; } @@ -532,7 +533,7 @@ static bool gsr_dbus_get_map(DBusMessageIter *resp_args, dict_entry *entries, in } if(!dbus_message_iter_next(&dictiter)) { - fprintf(stderr, "gsr error: gsr_dbus_get_map: missing entry value\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_dbus_get_map: missing entry value"); goto error; } @@ -575,7 +576,7 @@ int gsr_dbus_screencast_create_session(gsr_dbus *self, char **session_handle) { DBusMessage *response_msg = NULL; if(!gsr_dbus_call_screencast_method(self, "CreateSession", NULL, NULL, args, 2, NULL, &response_msg)) { - fprintf(stderr, "gsr error: gsr_dbus_screencast_create_session: failed to setup ScreenCast session. Make sure you have a desktop portal running with support for the ScreenCast interface and that the desktop portal matches the Wayland compositor you are running.\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_dbus_screencast_create_session: failed to setup ScreenCast session. Make sure you have a desktop portal running with support for the ScreenCast interface and that the desktop portal matches the Wayland compositor you are running."); return -1; } @@ -584,7 +585,7 @@ int gsr_dbus_screencast_create_session(gsr_dbus *self, char **session_handle) { //fprintf(stderr, "signature: %s, sender: %s\n", dbus_message_get_signature(msg), dbus_message_get_sender(msg)); DBusMessageIter resp_args; if(!dbus_message_iter_init(response_msg, &resp_args)) { - fprintf(stderr, "gsr error: gsr_dbus_screencast_create_session: missing response\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_dbus_screencast_create_session: missing response"); dbus_message_unref(response_msg); return -1; } @@ -605,7 +606,7 @@ int gsr_dbus_screencast_create_session(gsr_dbus *self, char **session_handle) { } if(!entries[0].str) { - fprintf(stderr, "gsr error: gsr_dbus_screencast_create_session: missing \"session_handle\" in response\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_dbus_screencast_create_session: missing \"session_handle\" in response"); dbus_message_unref(response_msg); return -1; } @@ -644,16 +645,16 @@ int gsr_dbus_screencast_select_sources(gsr_dbus *self, const char *session_handl uint32_t available_source_types = 0; gsr_dbus_desktop_portal_get_property(self, "org.freedesktop.portal.ScreenCast", "AvailableSourceTypes", &available_source_types); if(available_source_types == 0) - fprintf(stderr, "gsr error: gsr_dbus_screencast_select_sources: no source types are available\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_dbus_screencast_select_sources: no source types are available"); capture_type = unset_unsupported_capture_types(capture_type, available_source_types); uint32_t available_cursor_modes = 0; gsr_dbus_desktop_portal_get_property(self, "org.freedesktop.portal.ScreenCast", "AvailableCursorModes", &available_cursor_modes); if(available_cursor_modes == 0) - fprintf(stderr, "gsr warning: gsr_dbus_screencast_select_sources: no cursors modes are available\n"); + gsr_log(GSR_LOG_LEVEL_WARNING, "gsr_dbus_screencast_select_sources: no cursors modes are available"); if(cursor_mode == GSR_PORTAL_CURSOR_MODE_METADATA && !(available_cursor_modes & GSR_PORTAL_CURSOR_MODE_METADATA)) { - fprintf(stderr, "gsr warning: gsr_dbus_screencast_select_sources: cursor mode metadata is not available, using cursor mode embedded instead\n"); + gsr_log(GSR_LOG_LEVEL_WARNING, "gsr_dbus_screencast_select_sources: cursor mode metadata is not available, using cursor mode embedded instead"); cursor_mode = GSR_PORTAL_CURSOR_MODE_EMBEDDED; } cursor_mode = unset_unsupported_cursor_modes(cursor_mode, available_cursor_modes); @@ -696,14 +697,14 @@ int gsr_dbus_screencast_select_sources(gsr_dbus *self, const char *session_handl args[5].str = self->screencast_restore_token; } } else if(self->screencast_restore_token && self->screencast_restore_token[0]) { - fprintf(stderr, "gsr warning: gsr_dbus_screencast_select_sources: tried to use restore token but this option is only available in screencast version >= 4, your wayland compositors screencast version is %d\n", screencast_server_version); + gsr_log(GSR_LOG_LEVEL_WARNING, "gsr_dbus_screencast_select_sources: tried to use restore token but this option is only available in screencast version >= 4, your wayland compositors screencast version is %d", screencast_server_version); } DBusMessage *response_msg = NULL; if(!gsr_dbus_call_screencast_method(self, "SelectSources", session_handle, NULL, args, num_arg_dict, NULL, &response_msg)) { if(num_arg_dict == 6) { /* We dont know what the error exactly is but assume it may be because of invalid restore token. In that case try without restore token */ - fprintf(stderr, "gsr warning: gsr_dbus_screencast_select_sources: SelectSources failed, retrying without restore_token\n"); + gsr_log(GSR_LOG_LEVEL_WARNING, "gsr_dbus_screencast_select_sources: SelectSources failed, retrying without restore_token"); num_arg_dict = 5; if(!gsr_dbus_call_screencast_method(self, "SelectSources", session_handle, NULL, args, num_arg_dict, NULL, &response_msg)) return -1; @@ -716,7 +717,7 @@ int gsr_dbus_screencast_select_sources(gsr_dbus *self, const char *session_handl //fprintf(stderr, "signature: %s, sender: %s\n", dbus_message_get_signature(msg), dbus_message_get_sender(msg)); DBusMessageIter resp_args; if(!dbus_message_iter_init(response_msg, &resp_args)) { - fprintf(stderr, "gsr error: gsr_dbus_screencast_create_session: missing response\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_dbus_screencast_create_session: missing response"); dbus_message_unref(response_msg); return -1; } @@ -770,7 +771,7 @@ int gsr_dbus_screencast_start(gsr_dbus *self, const char *session_handle, uint32 //fprintf(stderr, "signature: %s, sender: %s\n", dbus_message_get_signature(msg), dbus_message_get_sender(msg)); DBusMessageIter resp_args; if(!dbus_message_iter_init(response_msg, &resp_args)) { - fprintf(stderr, "gsr error: gsr_dbus_screencast_start: missing response\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_dbus_screencast_start: missing response"); dbus_message_unref(response_msg); return -1; } @@ -782,7 +783,7 @@ int gsr_dbus_screencast_start(gsr_dbus *self, const char *session_handle, uint32 } if(dbus_message_iter_get_arg_type(&resp_args) != DBUS_TYPE_ARRAY) { - fprintf(stderr, "gsr error: gsr_dbus_screencast_start: missing array in response\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_dbus_screencast_start: missing array in response"); dbus_message_unref(response_msg); return -1; } @@ -798,31 +799,31 @@ int gsr_dbus_screencast_start(gsr_dbus *self, const char *session_handle, uint32 // dbus_message_iter_get_arg_type(&subiter), // dbus_message_iter_get_signature(&subiter)); if(dbus_message_iter_get_arg_type(&subiter) != DBUS_TYPE_DICT_ENTRY) { - fprintf(stderr, "gsr error: gsr_dbus_screencast_start: array value is not an entry\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_dbus_screencast_start: array value is not an entry"); goto error; } dbus_message_iter_recurse(&subiter, &dictiter); if(dbus_message_iter_get_arg_type(&dictiter) != DBUS_TYPE_STRING) { - fprintf(stderr, "gsr error: gsr_dbus_screencast_start: entry key is not a string\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_dbus_screencast_start: entry key is not a string"); goto error; } dbus_message_iter_get_basic(&dictiter, &key); if(!key) { - fprintf(stderr, "gsr error: gsr_dbus_screencast_start: failed to get entry key as value\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_dbus_screencast_start: failed to get entry key as value"); goto error; } if(strcmp(key, "restore_token") == 0) { if(!dbus_message_iter_next(&dictiter)) { - fprintf(stderr, "gsr error: gsr_dbus_screencast_start: missing restore_token value\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_dbus_screencast_start: missing restore_token value"); goto error; } if(dbus_message_iter_get_arg_type(&dictiter) != DBUS_TYPE_VARIANT) { - fprintf(stderr, "gsr error: gsr_dbus_screencast_start: restore_token is not a variant\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_dbus_screencast_start: restore_token is not a variant"); goto error; } @@ -830,7 +831,7 @@ int gsr_dbus_screencast_start(gsr_dbus *self, const char *session_handle, uint32 dbus_message_iter_recurse(&dictiter, &variant_iter); if(dbus_message_iter_get_arg_type(&variant_iter) != DBUS_TYPE_STRING) { - fprintf(stderr, "gsr error: gsr_dbus_screencast_start: restore_token is not a string\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_dbus_screencast_start: restore_token is not a string"); goto error; } @@ -847,12 +848,12 @@ int gsr_dbus_screencast_start(gsr_dbus *self, const char *session_handle, uint32 } } else if(strcmp(key, "streams") == 0) { if(!dbus_message_iter_next(&dictiter)) { - fprintf(stderr, "gsr error: gsr_dbus_screencast_start: missing streams value\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_dbus_screencast_start: missing streams value"); goto error; } if(dbus_message_iter_get_arg_type(&dictiter) != DBUS_TYPE_VARIANT) { - fprintf(stderr, "gsr error: gsr_dbus_screencast_start: streams value is not a variant\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_dbus_screencast_start: streams value is not a variant"); goto error; } @@ -860,7 +861,7 @@ int gsr_dbus_screencast_start(gsr_dbus *self, const char *session_handle, uint32 dbus_message_iter_recurse(&dictiter, &variant_iter); if(dbus_message_iter_get_arg_type(&variant_iter) != DBUS_TYPE_ARRAY) { - fprintf(stderr, "gsr error: gsr_dbus_screencast_start: streams value is not an array\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_dbus_screencast_start: streams value is not an array"); goto error; } @@ -881,7 +882,7 @@ int gsr_dbus_screencast_start(gsr_dbus *self, const char *session_handle, uint32 } if(*pipewire_node == 0) { - fprintf(stderr, "gsr error: gsr_dbus_screencast_start: no pipewire node returned\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_dbus_screencast_start: no pipewire node returned"); goto error; } diff --git a/src/egl.c b/src/egl.c index 3dcd5df..7e29bbc 100644 --- a/src/egl.c +++ b/src/egl.c @@ -1,4 +1,5 @@ #include "../include/egl.h" +#include "../include/log.h" #include "../include/window/window.h" #include "../include/library_loader.h" #include "../include/utils.h" @@ -53,34 +54,34 @@ static bool gsr_egl_create_window(gsr_egl *self, bool enable_debug) { self->egl_display = self->eglGetDisplay((EGLNativeDisplayType)gsr_window_get_display(self->window)); if(!self->egl_display) { - fprintf(stderr, "gsr error: gsr_egl_create_window failed: eglGetDisplay failed\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_egl_create_window failed: eglGetDisplay failed"); goto fail; } if(!self->eglInitialize(self->egl_display, NULL, NULL)) { - fprintf(stderr, "gsr error: gsr_egl_create_window failed: eglInitialize failed\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_egl_create_window failed: eglInitialize failed"); goto fail; } if(!self->eglChooseConfig(self->egl_display, attr, &ecfg, 1, &num_config) || num_config != 1) { - fprintf(stderr, "gsr error: gsr_egl_create_window failed: failed to find a matching config\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_egl_create_window failed: failed to find a matching config"); goto fail; } self->egl_context = self->eglCreateContext(self->egl_display, ecfg, NULL, ctxattr); if(!self->egl_context) { - fprintf(stderr, "gsr error: gsr_egl_create_window failed: failed to create egl context\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_egl_create_window failed: failed to create egl context"); goto fail; } self->egl_surface = self->eglCreateWindowSurface(self->egl_display, ecfg, (EGLNativeWindowType)gsr_window_get_window(self->window), NULL); if(!self->egl_surface) { - fprintf(stderr, "gsr error: gsr_egl_create_window failed: failed to create window surface\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_egl_create_window failed: failed to create window surface"); goto fail; } if(!self->eglMakeCurrent(self->egl_display, self->egl_surface, self->egl_surface, self->egl_context)) { - fprintf(stderr, "gsr error: gsr_egl_create_window failed: failed to make egl context current\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_egl_create_window failed: failed to make egl context current"); goto fail; } @@ -141,7 +142,7 @@ static bool gsr_egl_switch_to_glx_context(gsr_egl *self) { self->glx_fb_config = glx_fb_config_choose(self, display); if(!self->glx_fb_config) { - fprintf(stderr, "gsr error: gsr_egl_create_window failed: failed to find a suitable fb config\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_egl_create_window failed: failed to find a suitable fb config"); goto fail; } @@ -149,12 +150,12 @@ static bool gsr_egl_switch_to_glx_context(gsr_egl *self) { //self->glx_context = self->glXCreateContextAttribsARB(display, self->glx_fb_config, NULL, True, context_attrib_list); self->glx_context = self->glXCreateNewContext(display, self->glx_fb_config, GLX_RGBA_TYPE, NULL, True); if(!self->glx_context) { - fprintf(stderr, "gsr error: gsr_egl_create_window failed: failed to create glx context\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_egl_create_window failed: failed to create glx context"); goto fail; } if(!self->glXMakeContextCurrent(display, window, window, self->glx_context)) { - fprintf(stderr, "gsr error: gsr_egl_create_window failed: failed to make glx context current\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_egl_create_window failed: failed to make glx context current"); goto fail; } @@ -193,7 +194,7 @@ static bool gsr_egl_load_egl(gsr_egl *self, void *library) { }; if(!dlsym_load_list(library, required_dlsym)) { - fprintf(stderr, "gsr error: gsr_egl_load failed: missing required symbols in libEGL.so.1\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_egl_load failed: missing required symbols in libEGL.so.1"); return false; } @@ -221,17 +222,17 @@ static bool gsr_egl_proc_load_egl(gsr_egl *self) { self->glSignalSemaphoreEXT = (FUNC_glSignalSemaphoreEXT)self->eglGetProcAddress("glSignalSemaphoreEXT"); if(!self->eglExportDMABUFImageQueryMESA) { - fprintf(stderr, "gsr error: gsr_egl_load failed: could not find eglExportDMABUFImageQueryMESA\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_egl_load failed: could not find eglExportDMABUFImageQueryMESA"); return false; } if(!self->eglExportDMABUFImageMESA) { - fprintf(stderr, "gsr error: gsr_egl_load failed: could not find eglExportDMABUFImageMESA\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_egl_load failed: could not find eglExportDMABUFImageMESA"); return false; } if(!self->glEGLImageTargetTexture2DOES) { - fprintf(stderr, "gsr error: gsr_egl_load failed: could not find glEGLImageTargetTexture2DOES\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_egl_load failed: could not find glEGLImageTargetTexture2DOES"); return false; } @@ -251,13 +252,13 @@ static bool gsr_egl_load_glx(gsr_egl *self, void *library) { }; if(!dlsym_load_list(library, required_dlsym)) { - fprintf(stderr, "gsr error: gsr_egl_load failed: missing required symbols in libGLX.so.0\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_egl_load failed: missing required symbols in libGLX.so.0"); return false; } self->glXCreateContextAttribsARB = (FUNC_glXCreateContextAttribsARB)self->glXGetProcAddress((const unsigned char*)"glXCreateContextAttribsARB"); if(!self->glXCreateContextAttribsARB) { - fprintf(stderr, "gsr error: gsr_egl_load_glx failed: could not find glXCreateContextAttribsARB\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_egl_load_glx failed: could not find glXCreateContextAttribsARB"); return false; } @@ -343,7 +344,7 @@ static bool gsr_egl_load_gl(gsr_egl *self, void *library) { }; if(!dlsym_load_list(library, required_dlsym)) { - fprintf(stderr, "gsr error: gsr_egl_load failed: missing required symbols in libGL.so.1\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_egl_load failed: missing required symbols in libGL.so.1"); return false; } @@ -358,7 +359,7 @@ static void debug_callback(unsigned int source, unsigned int type, unsigned int (void)length; (void)userParam; if(severity != GL_DEBUG_SEVERITY_NOTIFICATION) - fprintf(stderr, "gsr info: gl callback: %s type = 0x%x, severity = 0x%x, message = %s\n", type == GL_DEBUG_TYPE_ERROR ? "** GL ERROR **" : "", type, severity, message); + gsr_log(GSR_LOG_LEVEL_INFO, "gl callback: %s type = 0x%x, severity = 0x%x, message = %s", type == GL_DEBUG_TYPE_ERROR ? "** GL ERROR **" : "", type, severity, message); } /* TODO: check for glx swap control extension string (GLX_EXT_swap_control, etc) */ @@ -378,12 +379,12 @@ static void set_vertical_sync_enabled(gsr_egl *egl, int enabled) { static int warned = 0; if (!warned) { warned = 1; - fprintf(stderr, "gsr warning: setting vertical sync not supported\n"); + gsr_log(GSR_LOG_LEVEL_WARNING, "setting vertical sync not supported"); } } if(result != 0) - fprintf(stderr, "gsr warning: setting vertical sync failed\n"); + gsr_log(GSR_LOG_LEVEL_WARNING, "setting vertical sync failed"); } static void gsr_egl_disable_vsync(gsr_egl *self) { @@ -407,7 +408,7 @@ bool gsr_egl_load(gsr_egl *self, gsr_window *window, bool is_monitor_capture, bo dlerror(); /* clear */ self->egl_library = dlopen("libEGL.so.1", RTLD_LAZY); if(!self->egl_library) { - fprintf(stderr, "gsr error: gsr_egl_load: failed to load libEGL.so.1, error: %s\n", dlerror()); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_egl_load: failed to load libEGL.so.1, error: %s", dlerror()); goto fail; } @@ -415,7 +416,7 @@ bool gsr_egl_load(gsr_egl *self, gsr_window *window, bool is_monitor_capture, bo self->gl_library = dlopen("libGL.so.1", RTLD_LAZY); if(!self->gl_library) { - fprintf(stderr, "gsr error: gsr_egl_load: failed to load libGL.so.1, error: %s\n", dlerror()); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_egl_load: failed to load libGL.so.1, error: %s", dlerror()); goto fail; } diff --git a/src/encoder/encoder.c b/src/encoder/encoder.c index 9ce7d19..2df4666 100644 --- a/src/encoder/encoder.c +++ b/src/encoder/encoder.c @@ -1,4 +1,5 @@ #include "../../include/encoder/encoder.h" +#include "../../include/log.h" #include "../../include/utils.h" #include @@ -25,7 +26,7 @@ static void gsr_write_first_frame_timestamp_file(const char *filepath) { FILE *file = fopen(filepath, "w"); if(!file) { - fprintf(stderr, "gsr warning: failed to open timestamp file '%s': %s\n", filepath, strerror(errno)); + gsr_log(GSR_LOG_LEVEL_WARNING, "failed to open timestamp file '%s': %s", filepath, strerror(errno)); return; } @@ -41,14 +42,14 @@ bool gsr_encoder_init(gsr_encoder *self, gsr_replay_storage replay_storage, size self->first_pts = -1; if(pthread_mutex_init(&self->file_write_mutex, NULL) != 0) { - fprintf(stderr, "gsr error: gsr_encoder_init: failed to create mutex\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_encoder_init: failed to create mutex"); gsr_encoder_deinit(self); return false; } self->file_write_mutex_created = true; if(pthread_mutex_init(&self->replay_mutex, NULL) != 0) { - fprintf(stderr, "gsr error: gsr_encoder_init: failed to create mutex\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_encoder_init: failed to create mutex"); gsr_encoder_deinit(self); return false; } @@ -57,7 +58,7 @@ bool gsr_encoder_init(gsr_encoder *self, gsr_replay_storage replay_storage, size if(replay_buffer_num_packets > 0) { self->replay_buffer = gsr_replay_buffer_create(replay_storage, replay_directory, replay_buffer_time, replay_buffer_num_packets); if(!self->replay_buffer) { - fprintf(stderr, "gsr error: gsr_encoder_init: failed to create replay buffer\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_encoder_init: failed to create replay buffer"); gsr_encoder_deinit(self); return false; } @@ -120,7 +121,7 @@ void gsr_encoder_receive_packets(gsr_encoder *self, AVCodecContext *codec_contex pthread_mutex_lock(&self->replay_mutex); const double time_now = clock_get_monotonic_seconds(); if(!gsr_replay_buffer_append(self->replay_buffer, av_packet, time_now)) - fprintf(stderr, "gsr error: gsr_encoder_receive_packets: failed to add replay buffer data\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_encoder_receive_packets: failed to add replay buffer data"); pthread_mutex_unlock(&self->replay_mutex); } @@ -155,7 +156,7 @@ void gsr_encoder_receive_packets(gsr_encoder *self, AVCodecContext *codec_contex char error_buffer[AV_ERROR_MAX_STRING_SIZE]; if(av_strerror(ret, error_buffer, sizeof(error_buffer)) < 0) snprintf(error_buffer, sizeof(error_buffer), "Unknown error"); - fprintf(stderr, "gsr error: gsr_encoder_receive_packets: failed to write frame index %d to muxer, reason: %s (%d)\n", av_packet->stream_index, error_buffer, ret); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_encoder_receive_packets: failed to write frame index %d to muxer, reason: %s (%d)", av_packet->stream_index, error_buffer, ret); } } pthread_mutex_unlock(&self->file_write_mutex); @@ -167,11 +168,11 @@ void gsr_encoder_receive_packets(gsr_encoder *self, AVCodecContext *codec_contex break; } else if (res == AVERROR_EOF) { // this is the end of the stream av_packet_free(&av_packet); - fprintf(stderr, "End of stream!\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_encoder_receive_packets: end of stream"); break; } else { av_packet_free(&av_packet); - fprintf(stderr, "Unexpected error: %d\n", res); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_encoder_receive_packets: unexpected error: %d", res); break; } } @@ -179,13 +180,13 @@ void gsr_encoder_receive_packets(gsr_encoder *self, AVCodecContext *codec_contex size_t gsr_encoder_add_recording_destination(gsr_encoder *self, AVCodecContext *codec_context, AVFormatContext *format_context, AVStream *stream, int64_t start_pts) { if(self->num_recording_destinations >= GSR_MAX_RECORDING_DESTINATIONS) { - fprintf(stderr, "gsr error: gsr_encoder_add_recording_destination: failed to add destination, reached the max amount of recording destinations (%d)\n", GSR_MAX_RECORDING_DESTINATIONS); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_encoder_add_recording_destination: failed to add destination, reached the max amount of recording destinations (%d)", GSR_MAX_RECORDING_DESTINATIONS); return (size_t)-1; } for(size_t i = 0; i < self->num_recording_destinations; ++i) { if(self->recording_destinations[i].stream == stream) { - fprintf(stderr, "gsr error: gsr_encoder_add_recording_destination: failed to add destination, the stream %p already exists as an output\n", (void*)stream); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_encoder_add_recording_destination: failed to add destination, the stream %p already exists as an output", (void*)stream); return (size_t)-1; } } diff --git a/src/encoder/video/nvenc.c b/src/encoder/video/nvenc.c index dd877c5..f7fca41 100644 --- a/src/encoder/video/nvenc.c +++ b/src/encoder/video/nvenc.c @@ -1,4 +1,5 @@ #include "../../../include/encoder/video/nvenc.h" +#include "../../../include/log.h" #include "../../../include/egl.h" #include "../../../include/cuda.h" #include "../../../include/utils.h" @@ -25,7 +26,7 @@ typedef struct { static bool gsr_video_encoder_nvenc_setup_context(gsr_video_encoder_nvenc *self, AVCodecContext *video_codec_context) { self->device_ctx = av_hwdevice_ctx_alloc(AV_HWDEVICE_TYPE_CUDA); if(!self->device_ctx) { - fprintf(stderr, "gsr error: gsr_video_encoder_nvenc_setup_context failed: failed to create hardware device context\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_video_encoder_nvenc_setup_context failed: failed to create hardware device context"); return false; } @@ -33,14 +34,14 @@ static bool gsr_video_encoder_nvenc_setup_context(gsr_video_encoder_nvenc *self, AVCUDADeviceContext *cuda_device_context = (AVCUDADeviceContext*)hw_device_context->hwctx; cuda_device_context->cuda_ctx = self->cuda.cu_ctx; if(av_hwdevice_ctx_init(self->device_ctx) < 0) { - fprintf(stderr, "gsr error: gsr_video_encoder_nvenc_setup_context failed: failed to create hardware device context\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_video_encoder_nvenc_setup_context failed: failed to create hardware device context"); av_buffer_unref(&self->device_ctx); return false; } AVBufferRef *frame_context = av_hwframe_ctx_alloc(self->device_ctx); if(!frame_context) { - fprintf(stderr, "gsr error: gsr_video_encoder_nvenc_setup_context failed: failed to create hwframe context\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_video_encoder_nvenc_setup_context failed: failed to create hwframe context"); av_buffer_unref(&self->device_ctx); return false; } @@ -53,8 +54,7 @@ static bool gsr_video_encoder_nvenc_setup_context(gsr_video_encoder_nvenc *self, hw_frame_context->device_ctx = (AVHWDeviceContext*)self->device_ctx->data; if (av_hwframe_ctx_init(frame_context) < 0) { - fprintf(stderr, "gsr error: gsr_video_encoder_nvenc_setup_context failed: failed to initialize hardware frame context " - "(note: ffmpeg version needs to be > 4.0)\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_video_encoder_nvenc_setup_context failed: failed to initialize hardware frame context (note: ffmpeg version needs to be > 4.0)"); av_buffer_unref(&self->device_ctx); //av_buffer_unref(&frame_context); return false; @@ -72,7 +72,7 @@ static bool cuda_register_opengl_texture(gsr_cuda *cuda, CUgraphicsResource *cud if (res != CUDA_SUCCESS) { const char *err_str = "unknown"; cuda->cuGetErrorString(res, &err_str); - fprintf(stderr, "gsr error: cuda_register_opengl_texture: cuGraphicsGLRegisterImage failed, error: %s, texture " "id: %u\n", err_str, texture_id); + gsr_log(GSR_LOG_LEVEL_ERROR, "cuda_register_opengl_texture: cuGraphicsGLRegisterImage failed, error: %s, texture id: %u", err_str, texture_id); return false; } @@ -86,7 +86,7 @@ static bool cuda_register_opengl_texture(gsr_cuda *cuda, CUgraphicsResource *cud static bool gsr_video_encoder_nvenc_setup_textures(gsr_video_encoder_nvenc *self, AVCodecContext *video_codec_context, AVFrame *frame) { const int res = av_hwframe_get_buffer(video_codec_context->hw_frames_ctx, frame, 0); if(res < 0) { - fprintf(stderr, "gsr error: gsr_video_encoder_nvenc_setup_textures: av_hwframe_get_buffer failed: %d\n", res); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_video_encoder_nvenc_setup_textures: av_hwframe_get_buffer failed: %d", res); return false; } @@ -99,7 +99,7 @@ static bool gsr_video_encoder_nvenc_setup_textures(gsr_video_encoder_nvenc *self self->target_texture_size[i] = (vec2i){ video_codec_context->width / div[i], video_codec_context->height / div[i] }; self->target_textures[i] = gl_create_texture(self->params.egl, self->target_texture_size[i].x, self->target_texture_size[i].y, self->params.color_depth == GSR_COLOR_DEPTH_8_BITS ? internal_formats_nv12[i] : internal_formats_p010[i], formats[i], GL_NEAREST); if(self->target_textures[i] == 0) { - fprintf(stderr, "gsr error: gsr_video_encoder_nvenc_setup_textures: failed to create opengl texture\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_video_encoder_nvenc_setup_textures: failed to create opengl texture"); return false; } @@ -117,7 +117,7 @@ static bool gsr_video_encoder_nvenc_start(gsr_video_encoder *encoder, AVCodecCon gsr_video_encoder_nvenc *self = encoder->priv; if(!gsr_cuda_load(&self->cuda)) { - fprintf(stderr, "gsr error: gsr_video_encoder_nvenc_start: failed to load cuda\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_video_encoder_nvenc_start: failed to load cuda"); gsr_video_encoder_nvenc_stop(self, video_codec_context); return false; } diff --git a/src/encoder/video/software.c b/src/encoder/video/software.c index b473bb9..5ee3ab9 100644 --- a/src/encoder/video/software.c +++ b/src/encoder/video/software.c @@ -1,4 +1,5 @@ #include "../../../include/encoder/video/software.h" +#include "../../../include/log.h" #include "../../../include/egl.h" #include "../../../include/utils.h" @@ -19,13 +20,13 @@ typedef struct { static bool gsr_video_encoder_software_setup_textures(gsr_video_encoder_software *self, AVCodecContext *video_codec_context, AVFrame *frame) { int res = av_frame_get_buffer(frame, LINESIZE_ALIGNMENT); if(res < 0) { - fprintf(stderr, "gsr error: gsr_video_encoder_software_setup_textures: av_frame_get_buffer failed: %d\n", res); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_video_encoder_software_setup_textures: av_frame_get_buffer failed: %d", res); return false; } res = av_frame_make_writable(frame); if(res < 0) { - fprintf(stderr, "gsr error: gsr_video_encoder_software_setup_textures: av_frame_make_writable failed: %d\n", res); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_video_encoder_software_setup_textures: av_frame_make_writable failed: %d", res); return false; } @@ -38,7 +39,7 @@ static bool gsr_video_encoder_software_setup_textures(gsr_video_encoder_software self->texture_sizes[i] = (vec2i){ video_codec_context->width / div[i], video_codec_context->height / div[i] }; self->target_textures[i] = gl_create_texture(self->params.egl, self->texture_sizes[i].x, self->texture_sizes[i].y, self->params.color_depth == GSR_COLOR_DEPTH_8_BITS ? internal_formats_nv12[i] : internal_formats_p010[i], formats[i], GL_NEAREST); if(self->target_textures[i] == 0) { - fprintf(stderr, "gsr error: gsr_capture_kms_setup_cuda_textures: failed to create opengl texture\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_capture_kms_setup_cuda_textures: failed to create opengl texture"); return false; } } diff --git a/src/encoder/video/vaapi.c b/src/encoder/video/vaapi.c index 449fb1d..d310125 100644 --- a/src/encoder/video/vaapi.c +++ b/src/encoder/video/vaapi.c @@ -1,4 +1,5 @@ #include "../../../include/encoder/video/vaapi.h" +#include "../../../include/log.h" #include "../../../include/utils.h" #include "../../../include/egl.h" @@ -28,18 +29,18 @@ typedef struct { static bool gsr_video_encoder_vaapi_setup_context(gsr_video_encoder_vaapi *self, AVCodecContext *video_codec_context) { char render_path[128]; if(!gsr_card_path_get_render_path(self->params.egl->card_path, render_path)) { - fprintf(stderr, "gsr error: gsr_video_encoder_vaapi_setup_context: failed to get /dev/dri/renderDXXX file from %s\n", self->params.egl->card_path); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_video_encoder_vaapi_setup_context: failed to get /dev/dri/renderDXXX file from %s", self->params.egl->card_path); return false; } if(av_hwdevice_ctx_create(&self->device_ctx, AV_HWDEVICE_TYPE_VAAPI, render_path, NULL, 0) < 0) { - fprintf(stderr, "gsr error: gsr_video_encoder_vaapi_setup_context: failed to create hardware device context\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_video_encoder_vaapi_setup_context: failed to create hardware device context"); return false; } AVBufferRef *frame_context = av_hwframe_ctx_alloc(self->device_ctx); if(!frame_context) { - fprintf(stderr, "gsr error: gsr_video_encoder_vaapi_setup_context: failed to create hwframe context\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_video_encoder_vaapi_setup_context: failed to create hwframe context"); av_buffer_unref(&self->device_ctx); return false; } @@ -57,8 +58,7 @@ static bool gsr_video_encoder_vaapi_setup_context(gsr_video_encoder_vaapi *self, self->va_dpy = vactx->display; if (av_hwframe_ctx_init(frame_context) < 0) { - fprintf(stderr, "gsr error: gsr_video_encoder_vaapi_setup_context: failed to initialize hardware frame context " - "(note: ffmpeg version needs to be > 4.0)\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_video_encoder_vaapi_setup_context: failed to initialize hardware frame context (note: ffmpeg version needs to be > 4.0)"); av_buffer_unref(&self->device_ctx); //av_buffer_unref(&frame_context); return false; @@ -76,7 +76,7 @@ static uint32_t fourcc(uint32_t a, uint32_t b, uint32_t c, uint32_t d) { static bool gsr_video_encoder_vaapi_setup_textures(gsr_video_encoder_vaapi *self, AVCodecContext *video_codec_context, AVFrame *frame) { const int res = av_hwframe_get_buffer(video_codec_context->hw_frames_ctx, frame, 0); if(res < 0) { - fprintf(stderr, "gsr error: gsr_video_encoder_vaapi_setup_textures: av_hwframe_get_buffer failed: %d\n", res); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_video_encoder_vaapi_setup_textures: av_hwframe_get_buffer failed: %d", res); return false; } @@ -84,7 +84,7 @@ static bool gsr_video_encoder_vaapi_setup_textures(gsr_video_encoder_vaapi *self VAStatus va_status = vaExportSurfaceHandle(self->va_dpy, target_surface_id, VA_SURFACE_ATTRIB_MEM_TYPE_DRM_PRIME_2, VA_EXPORT_SURFACE_WRITE_ONLY | VA_EXPORT_SURFACE_SEPARATE_LAYERS, &self->prime); if(va_status != VA_STATUS_SUCCESS) { - fprintf(stderr, "gsr error: gsr_video_encoder_vaapi_setup_textures: vaExportSurfaceHandle failed, error: %d\n", va_status); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_video_encoder_vaapi_setup_textures: vaExportSurfaceHandle failed, error: %d", va_status); return false; } vaSyncSurface(self->va_dpy, target_surface_id); @@ -120,7 +120,7 @@ static bool gsr_video_encoder_vaapi_setup_textures(gsr_video_encoder_vaapi *self while(self->params.egl->eglGetError() != EGL_SUCCESS){} EGLImage image = self->params.egl->eglCreateImage(self->params.egl->egl_display, 0, EGL_LINUX_DMA_BUF_EXT, NULL, img_attr); if(!image) { - fprintf(stderr, "gsr error: gsr_video_encoder_vaapi_setup_textures: failed to create egl image from drm fd for output drm fd, error: %d\n", self->params.egl->eglGetError()); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_video_encoder_vaapi_setup_textures: failed to create egl image from drm fd for output drm fd, error: %d", self->params.egl->eglGetError()); return false; } @@ -133,7 +133,7 @@ static bool gsr_video_encoder_vaapi_setup_textures(gsr_video_encoder_vaapi *self self->params.egl->glEGLImageTargetTexture2DOES(GL_TEXTURE_2D, image); if(self->params.egl->glGetError() != 0 || self->params.egl->eglGetError() != EGL_SUCCESS) { // TODO: Get the error properly - fprintf(stderr, "gsr error: gsr_video_encoder_vaapi_setup_textures: failed to bind egl image to gl texture, error: %d\n", self->params.egl->eglGetError()); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_video_encoder_vaapi_setup_textures: failed to bind egl image to gl texture, error: %d", self->params.egl->eglGetError()); self->params.egl->eglDestroyImage(self->params.egl->egl_display, image); self->params.egl->glBindTexture(GL_TEXTURE_2D, 0); return false; @@ -145,7 +145,7 @@ static bool gsr_video_encoder_vaapi_setup_textures(gsr_video_encoder_vaapi *self return true; } else { - fprintf(stderr, "gsr error: gsr_video_encoder_vaapi_setup_textures: unexpected fourcc %u for output drm fd, expected nv12 or p010\n", self->prime.fourcc); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_video_encoder_vaapi_setup_textures: unexpected fourcc %u for output drm fd, expected nv12 or p010", self->prime.fourcc); return false; } } @@ -165,26 +165,26 @@ static bool supports_hevc_without_padding(const char *card_path) { char render_path[128]; if(!gsr_card_path_get_render_path(card_path, render_path)) { - fprintf(stderr, "gsr error: supports_hevc_without_padding: failed to get /dev/dri/renderDXXX file from %s\n", card_path); + gsr_log(GSR_LOG_LEVEL_ERROR, "supports_hevc_without_padding: failed to get /dev/dri/renderDXXX file from %s", card_path); return false; } const int drm_fd = open(render_path, O_RDWR); if(drm_fd == -1) { - fprintf(stderr, "gsr error: supports_hevc_without_padding: failed to open device %s\n", render_path); + gsr_log(GSR_LOG_LEVEL_ERROR, "supports_hevc_without_padding: failed to open device %s", render_path); return false; } const VADisplay va_dpy = vaGetDisplayDRM(drm_fd); if(!va_dpy) { - fprintf(stderr, "gsr error: supports_hevc_without_padding: failed to get vaapi display for device %s\n", render_path); + gsr_log(GSR_LOG_LEVEL_ERROR, "supports_hevc_without_padding: failed to get vaapi display for device %s", render_path); goto done; } vaSetInfoCallback(va_dpy, NULL, NULL); if(vaInitialize(va_dpy, &va_major, &va_minor) != VA_STATUS_SUCCESS) { - fprintf(stderr, "gsr error: supports_hevc_without_padding: vaInitialize failed\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "supports_hevc_without_padding: vaInitialize failed"); goto done; } initialized = true; @@ -193,26 +193,26 @@ static bool supports_hevc_without_padding(const char *card_path) { if(va_status != VA_STATUS_SUCCESS) { va_status = vaCreateConfig(va_dpy, VAProfileHEVCMain, VAEntrypointEncSliceLP, NULL, 0, &va_config); if(va_status != VA_STATUS_SUCCESS) { - fprintf(stderr, "gsr error: supports_hevc_without_padding: failed to create hevc vaapi config, error: %s (%d)\n", vaErrorStr(va_status), va_status); + gsr_log(GSR_LOG_LEVEL_ERROR, "supports_hevc_without_padding: failed to create hevc vaapi config, error: %s (%d)", vaErrorStr(va_status), va_status); return false; } } va_status = vaQuerySurfaceAttributes(va_dpy, va_config, 0, &num_surface_attr); if(va_status != VA_STATUS_SUCCESS) { - fprintf(stderr, "gsr error: supports_hevc_without_padding: failed to query vaapi surface attributes size, error: %s (%d)\n", vaErrorStr(va_status), va_status); + gsr_log(GSR_LOG_LEVEL_ERROR, "supports_hevc_without_padding: failed to query vaapi surface attributes size, error: %s (%d)", vaErrorStr(va_status), va_status); goto done; } surface_attr_list = malloc(num_surface_attr * sizeof(VASurfaceAttrib)); if(!surface_attr_list) { - fprintf(stderr, "gsr error: supports_hevc_without_padding: failed to allocate memory for %u vaapi surface attributes, error: %s (%d)\n", num_surface_attr, vaErrorStr(va_status), va_status); + gsr_log(GSR_LOG_LEVEL_ERROR, "supports_hevc_without_padding: failed to allocate memory for %u vaapi surface attributes, error: %s (%d)", num_surface_attr, vaErrorStr(va_status), va_status); goto done; } va_status = vaQuerySurfaceAttributes(va_dpy, va_config, surface_attr_list, &num_surface_attr); if(va_status != VA_STATUS_SUCCESS) { - fprintf(stderr, "gsr error: supports_hevc_without_padding: failed to query vaapi surface attributes data, error: %s (%d)\n", vaErrorStr(va_status), va_status); + gsr_log(GSR_LOG_LEVEL_ERROR, "supports_hevc_without_padding: failed to query vaapi surface attributes data, error: %s (%d)", vaErrorStr(va_status), va_status); goto done; } @@ -264,7 +264,7 @@ static bool gsr_video_encoder_vaapi_start(gsr_video_encoder *encoder, AVCodecCon } if(FFALIGN(video_codec_context->width, 2) != FFALIGN(frame->width, 2) || FFALIGN(video_codec_context->height, 2) != FFALIGN(frame->height, 2)) { - fprintf(stderr, "gsr warning: gsr_video_encoder_vaapi_start: black bars have been added to the video because of a bug in AMD drivers/hardware. Record with h264/hevc codec instead (-k h264) to get around this issue\n"); + gsr_log(GSR_LOG_LEVEL_WARNING, "gsr_video_encoder_vaapi_start: black bars have been added to the video because of a bug in AMD drivers/hardware. Record with h264/hevc codec instead (-k h264) to get around this issue"); } if(video_codec_context->width < 128) diff --git a/src/encoder/video/vulkan.c b/src/encoder/video/vulkan.c index 3b7c567..f2872d0 100644 --- a/src/encoder/video/vulkan.c +++ b/src/encoder/video/vulkan.c @@ -1,4 +1,5 @@ #include "../../../include/encoder/video/vulkan.h" +#include "../../../include/log.h" #include "../../../include/utils.h" #include "../../../include/egl.h" @@ -73,12 +74,12 @@ typedef struct { static bool gsr_vk_funcs_load(gsr_vk_funcs *vk, PFN_vkGetInstanceProcAddr get_inst_proc, VkInstance inst, VkDevice dev) { PFN_vkGetDeviceProcAddr get_dev_proc = (PFN_vkGetDeviceProcAddr)get_inst_proc(inst, "vkGetDeviceProcAddr"); if(!get_dev_proc) { - fprintf(stderr, "gsr error: gsr_vk_funcs_load: failed to load vkGetDeviceProcAddr\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_vk_funcs_load: failed to load vkGetDeviceProcAddr"); return false; } -#define LOAD_INST(name) vk->name = (PFN_##name)get_inst_proc(inst, #name); if(!vk->name) { fprintf(stderr, "gsr error: gsr_vk_funcs_load: failed to load " #name "\n"); return false; } -#define LOAD_DEV(name) vk->name = (PFN_##name)get_dev_proc(dev, #name); if(!vk->name) { fprintf(stderr, "gsr error: gsr_vk_funcs_load: failed to load " #name "\n"); return false; } +#define LOAD_INST(name) vk->name = (PFN_##name)get_inst_proc(inst, #name); if(!vk->name) { gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_vk_funcs_load: failed to load " #name); return false; } +#define LOAD_DEV(name) vk->name = (PFN_##name)get_dev_proc(dev, #name); if(!vk->name) { gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_vk_funcs_load: failed to load " #name); return false; } LOAD_INST(vkGetPhysicalDeviceMemoryProperties) LOAD_DEV(vkCreateImage) @@ -118,13 +119,13 @@ static bool gsr_video_encoder_vulkan_setup_context(gsr_video_encoder_vulkan *sel snprintf(device_index_str, sizeof(device_index_str), "%d", self->params.egl->vulkan_device_index); if(av_hwdevice_ctx_create(&self->device_ctx, AV_HWDEVICE_TYPE_VULKAN, device_index_str, options, 0) < 0) { - fprintf(stderr, "gsr error: gsr_video_encoder_vulkan_setup_context: failed to create hardware device context\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_video_encoder_vulkan_setup_context: failed to create hardware device context"); return false; } AVBufferRef *frame_context = av_hwframe_ctx_alloc(self->device_ctx); if(!frame_context) { - fprintf(stderr, "gsr error: gsr_video_encoder_vulkan_setup_context: failed to create hwframe context\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_video_encoder_vulkan_setup_context: failed to create hwframe context"); av_buffer_unref(&self->device_ctx); return false; } @@ -137,8 +138,7 @@ static bool gsr_video_encoder_vulkan_setup_context(gsr_video_encoder_vulkan *sel hw_frame_context->device_ctx = (AVHWDeviceContext*)self->device_ctx->data; if (av_hwframe_ctx_init(frame_context) < 0) { - fprintf(stderr, "gsr error: gsr_video_encoder_vulkan_setup_context: failed to initialize hardware frame context " - "(note: ffmpeg version needs to be > 4.0)\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_video_encoder_vulkan_setup_context: failed to initialize hardware frame context (note: ffmpeg version needs to be > 4.0)"); av_buffer_unref(&self->device_ctx); return false; } @@ -230,7 +230,7 @@ static bool create_exportable_image( }; if(vk->vkCreateImage(dev, &img_info, NULL, out_image) != VK_SUCCESS) { - fprintf(stderr, "gsr error: create_exportable_image: vkCreateImage failed\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "create_exportable_image: vkCreateImage failed"); return false; } @@ -240,7 +240,7 @@ static bool create_exportable_image( uint32_t mem_type_idx = get_memory_type_idx(phys_dev, &mem_reqs, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, vk->vkGetPhysicalDeviceMemoryProperties); if(mem_type_idx == UINT32_MAX) { - fprintf(stderr, "gsr error: create_exportable_image: no suitable memory type\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "create_exportable_image: no suitable memory type"); vk->vkDestroyImage(dev, *out_image, NULL); *out_image = VK_NULL_HANDLE; return false; @@ -263,14 +263,14 @@ static bool create_exportable_image( }; if(vk->vkAllocateMemory(dev, &mem_alloc_info, NULL, out_memory) != VK_SUCCESS) { - fprintf(stderr, "gsr error: create_exportable_image: vkAllocateMemory failed\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "create_exportable_image: vkAllocateMemory failed"); vk->vkDestroyImage(dev, *out_image, NULL); *out_image = VK_NULL_HANDLE; return false; } if(vk->vkBindImageMemory(dev, *out_image, *out_memory, 0) != VK_SUCCESS) { - fprintf(stderr, "gsr error: create_exportable_image: vkBindImageMemory failed\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "create_exportable_image: vkBindImageMemory failed"); vk->vkFreeMemory(dev, *out_memory, NULL); vk->vkDestroyImage(dev, *out_image, NULL); *out_memory = VK_NULL_HANDLE; @@ -285,7 +285,7 @@ static bool create_exportable_image( static bool gsr_video_encoder_vulkan_setup_textures(gsr_video_encoder_vulkan *self, AVCodecContext *video_codec_context, AVFrame *frame) { const int res = av_hwframe_get_buffer(video_codec_context->hw_frames_ctx, frame, 0); if(res < 0) { - fprintf(stderr, "gsr error: gsr_video_encoder_vulkan_setup_textures: av_hwframe_get_buffer failed: %d\n", res); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_video_encoder_vulkan_setup_textures: av_hwframe_get_buffer failed: %d", res); return false; } @@ -293,7 +293,7 @@ static bool gsr_video_encoder_vulkan_setup_textures(gsr_video_encoder_vulkan *se AVVulkanDeviceContext *vv = video_codec_context_get_vulkan_data(video_codec_context); if(!vv) { - fprintf(stderr, "gsr error: gsr_video_encoder_vulkan_setup_textures: failed to get vulkan device context\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_video_encoder_vulkan_setup_textures: failed to get vulkan device context"); return false; } @@ -327,7 +327,7 @@ static bool gsr_video_encoder_vulkan_setup_textures(gsr_video_encoder_vulkan *se }; int fd = -1; if(self->vk.vkGetMemoryFdKHR(vv->act_dev, &fd_info, &fd) != VK_SUCCESS) { - fprintf(stderr, "gsr error: gsr_video_encoder_vulkan_setup_textures: vkGetMemoryFdKHR failed for plane %d\n", i); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_video_encoder_vulkan_setup_textures: vkGetMemoryFdKHR failed for plane %d", i); return false; } @@ -338,7 +338,7 @@ static bool gsr_video_encoder_vulkan_setup_textures(gsr_video_encoder_vulkan *se self->params.egl->glImportMemoryFdEXT(self->gl_memory_objects[i], self->export_memory_size[i], GL_HANDLE_TYPE_OPAQUE_FD_EXT, fd); if(!self->params.egl->glIsMemoryObjectEXT(self->gl_memory_objects[i])) { - fprintf(stderr, "gsr error: gsr_video_encoder_vulkan_setup_textures: failed to import memory FD for plane %d\n", i); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_video_encoder_vulkan_setup_textures: failed to import memory FD for plane %d", i); return false; } } @@ -370,7 +370,7 @@ static bool gsr_video_encoder_vulkan_setup_textures(gsr_video_encoder_vulkan *se .queueFamilyIndex = (uint32_t)get_graphics_queue_family(vv), }; if(self->vk.vkCreateCommandPool(vv->act_dev, &pool_info, NULL, &self->command_pool) != VK_SUCCESS) { - fprintf(stderr, "gsr error: gsr_video_encoder_vulkan_setup_textures: vkCreateCommandPool failed\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_video_encoder_vulkan_setup_textures: vkCreateCommandPool failed"); return false; } @@ -381,7 +381,7 @@ static bool gsr_video_encoder_vulkan_setup_textures(gsr_video_encoder_vulkan *se .commandBufferCount = 1, }; if(self->vk.vkAllocateCommandBuffers(vv->act_dev, &cb_alloc_info, &self->command_buffer) != VK_SUCCESS) { - fprintf(stderr, "gsr error: gsr_video_encoder_vulkan_setup_textures: vkAllocateCommandBuffers failed\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_video_encoder_vulkan_setup_textures: vkAllocateCommandBuffers failed"); return false; } @@ -389,7 +389,7 @@ static bool gsr_video_encoder_vulkan_setup_textures(gsr_video_encoder_vulkan *se .sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO, }; if(self->vk.vkCreateFence(vv->act_dev, &fence_info, NULL, &self->fence) != VK_SUCCESS) { - fprintf(stderr, "gsr error: gsr_video_encoder_vulkan_setup_textures: vkCreateFence failed\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_video_encoder_vulkan_setup_textures: vkCreateFence failed"); return false; } @@ -484,7 +484,7 @@ static bool gsr_video_encoder_vulkan_start(gsr_video_encoder *encoder, AVCodecCo } if(FFALIGN(video_codec_context->width, 2) != FFALIGN(frame->width, 2) || FFALIGN(video_codec_context->height, 2) != FFALIGN(frame->height, 2)) { - fprintf(stderr, "gsr warning: gsr_video_encoder_vulkan_start: black bars have been added to the video because of a bug in AMD drivers/hardware. Record with h264/hevc vulkan codec instead (-k h264_vulkan) to get around this issue\n"); + gsr_log(GSR_LOG_LEVEL_WARNING, "gsr_video_encoder_vulkan_start: black bars have been added to the video because of a bug in AMD drivers/hardware. Record with h264/hevc vulkan codec instead (-k h264_vulkan) to get around this issue"); } if(video_codec_context->width < 128) diff --git a/src/image_writer.c b/src/image_writer.c index adc88a7..5f3fe9e 100644 --- a/src/image_writer.c +++ b/src/image_writer.c @@ -1,4 +1,5 @@ #include "../include/image_writer.h" +#include "../include/log.h" #include "../include/egl.h" #include "../include/utils.h" @@ -83,7 +84,7 @@ bool gsr_image_writer_init_opengl(gsr_image_writer *self, gsr_egl *egl, int widt self->height = height; self->texture = gl_create_texture(self->egl, self->width, self->height, GL_RGBA8, GL_RGBA, GL_NEAREST); /* TODO: use GL_RGB16 instead of GL_RGB8 for hdr/10-bit */ if(self->texture == 0) { - fprintf(stderr, "gsr error: gsr_image_writer_init: failed to create texture\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_image_writer_init: failed to create texture"); return false; } @@ -128,7 +129,7 @@ static bool gsr_image_writer_write_memory_to_file(gsr_image_writer *self, const } if(!success) - fprintf(stderr, "gsr error: gsr_image_writer_write_to_file: failed to write image data to output file %s\n", filepath); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_image_writer_write_to_file: failed to write image data to output file %s", filepath); return success; } @@ -136,7 +137,7 @@ static bool gsr_image_writer_write_memory_to_file(gsr_image_writer *self, const static bool gsr_image_writer_write_opengl_texture_to_file(gsr_image_writer *self, const char *filepath, gsr_image_format image_format, int quality) { uint8_t *frame_data = malloc(self->width * self->height * 4); if(!frame_data) { - fprintf(stderr, "gsr error: gsr_image_writer_write_to_file: failed to allocate memory for image frame\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_image_writer_write_to_file: failed to allocate memory for image frame"); return false; } diff --git a/src/kde_night_light.c b/src/kde_night_light.c index d5c9578..587e0c8 100644 --- a/src/kde_night_light.c +++ b/src/kde_night_light.c @@ -1,4 +1,5 @@ #include "../include/kde_night_light.h" +#include "../include/log.h" #ifdef GSR_DBUS @@ -346,7 +347,7 @@ bool gsr_kde_night_light_get_inverse_matrix(gsr_kde_night_light *self, gsr_night #include gsr_kde_night_light* gsr_kde_night_light_create(void) { - fprintf(stderr, "gsr warning: kde night light handling disabled because gsr was compiled without pipewire (which also disables dbus)\n"); + gsr_log(GSR_LOG_LEVEL_WARNING, "kde night light handling disabled because gsr was compiled without pipewire (which also disables dbus)"); return NULL; } diff --git a/src/library_loader.c b/src/library_loader.c index 0aeee9b..37a1767 100644 --- a/src/library_loader.c +++ b/src/library_loader.c @@ -1,4 +1,5 @@ #include "../include/library_loader.h" +#include "../include/log.h" #include #include @@ -10,7 +11,7 @@ void* dlsym_print_fail(void *handle, const char *name, bool required) { char *err_str = dlerror(); if(!sym) - fprintf(stderr, "%s: dlsym(handle, \"%s\") failed, error: %s\n", required ? "error" : "warning", name, err_str ? err_str : "(null)"); + gsr_log(required ? GSR_LOG_LEVEL_ERROR : GSR_LOG_LEVEL_WARNING, "dlsym(handle, \"%s\") failed, error: %s", name, err_str ? err_str : "(null)"); return sym; } diff --git a/src/log.c b/src/log.c new file mode 100644 index 0000000..b3ebc49 --- /dev/null +++ b/src/log.c @@ -0,0 +1,47 @@ +#include "../include/log.h" + +#include +#include + +#define GSR_LOG_MESSAGE_MAX_SIZE 4096 + +static const char* log_level_to_string(gsr_log_level level) { + switch(level) { + case GSR_LOG_LEVEL_DEBUG: return "debug"; + case GSR_LOG_LEVEL_INFO: return "info"; + case GSR_LOG_LEVEL_WARNING: return "warning"; + case GSR_LOG_LEVEL_ERROR: return "error"; + } + return "unknown"; +} + +static void gsr_log_default_handler(gsr_log_level level, const char *message, void *userdata) { + (void)userdata; + fprintf(stderr, "gsr %s: %s\n", log_level_to_string(level), message); +} + +static gsr_log_level log_level = GSR_LOG_LEVEL_INFO; +static gsr_log_handler log_handler = gsr_log_default_handler; +static void *log_handler_userdata = NULL; + +void gsr_log(gsr_log_level level, const char *fmt, ...) { + if(level < log_level) + return; + + char message[GSR_LOG_MESSAGE_MAX_SIZE]; + va_list args; + va_start(args, fmt); + vsnprintf(message, sizeof(message), fmt, args); + va_end(args); + + log_handler(level, message, log_handler_userdata); +} + +void gsr_log_set_level(gsr_log_level level) { + log_level = level; +} + +void gsr_log_set_handler(gsr_log_handler handler, void *userdata) { + log_handler = handler ? handler : gsr_log_default_handler; + log_handler_userdata = userdata; +} diff --git a/src/main.cpp b/src/main.cpp index 06996c3..df609bb 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -31,6 +31,7 @@ extern "C" { #include "../kms/client/kms_client.h" } +#include "../include/log.h" #include #include #include @@ -196,7 +197,7 @@ static AVSampleFormat audio_codec_get_sample_format(AVCodecContext *audio_codec_ supports_s16 = false; if(!supports_s16 && !supports_flt) { - fprintf(stderr, "gsr warning: opus audio codec is chosen but your ffmpeg version does not support s16/flt sample format and performance might be slightly worse.\n"); + gsr_log(GSR_LOG_LEVEL_WARNING, "opus audio codec is chosen but your ffmpeg version does not support s16/flt sample format and performance might be slightly worse."); fprintf(stderr, " You can either rebuild ffmpeg with libopus instead of the built-in opus, use the flatpak version of gpu screen recorder or record with aac audio codec instead (-ac aac).\n"); fprintf(stderr, " Falling back to fltp audio sample format instead.\n"); } @@ -250,7 +251,7 @@ static AVCodecContext* create_audio_codec_context(int fps, gsr_audio_codec audio (void)fps; const AVCodec *codec = avcodec_find_encoder(audio_codec_get_id(audio_codec)); if (!codec) { - fprintf(stderr, "gsr error: Could not find %s audio encoder\n", audio_codec_get_name(audio_codec)); + gsr_log(GSR_LOG_LEVEL_ERROR, "Could not find %s audio encoder", audio_codec_get_name(audio_codec)); _exit(1); } @@ -578,7 +579,7 @@ static void open_video_software(AVCodecContext *codec_context, const args_parser int ret = avcodec_open2(codec_context, codec_context->codec, &options); if (ret < 0) { - fprintf(stderr, "gsr error: Could not open video codec: %s\n", av_error_to_string(ret)); + gsr_log(GSR_LOG_LEVEL_ERROR, "Could not open video codec: %s", av_error_to_string(ret)); _exit(1); } } @@ -728,7 +729,7 @@ static void open_video_hardware(AVCodecContext *codec_context, bool low_power, c int ret = avcodec_open2(codec_context, codec_context->codec, &options); if (ret < 0) { - fprintf(stderr, "gsr error: Could not open video codec: %s\n", av_error_to_string(ret)); + gsr_log(GSR_LOG_LEVEL_ERROR, "Could not open video codec: %s", av_error_to_string(ret)); _exit(1); } } @@ -807,7 +808,7 @@ static std::string get_time_only_str() { static AVStream* create_stream(AVFormatContext *av_format_context, AVCodecContext *codec_context) { AVStream *stream = avformat_new_stream(av_format_context, nullptr); if (!stream) { - fprintf(stderr, "gsr error: Could not allocate stream\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "Could not allocate stream"); _exit(1); } stream->id = av_format_context->nb_streams - 1; @@ -821,7 +822,7 @@ static void run_recording_saved_script_async(const char *script_file, const char char script_file_full[PATH_MAX]; script_file_full[0] = '\0'; if(!realpath(script_file, script_file_full)) { - fprintf(stderr, "gsr error: script file not found: %s\n", script_file); + gsr_log(GSR_LOG_LEVEL_ERROR, "script file not found: %s", script_file); return; } @@ -966,7 +967,7 @@ static void set_format_context_options(AVFormatContext *av_format_context) { if(strcmp(file_extension, "mp4") != 0 && strcmp(file_extension, "mov") != 0) return; - fprintf(stderr, "gsr warning: your FFmpeg version is known to be buggy (it doesn't have working hybrid_fragmented movflags). If you experience stutter in the mp4 file then update your FFmpeg to at least version 8 or record to a mkv file\n"); + gsr_log(GSR_LOG_LEVEL_WARNING, "your FFmpeg version is known to be buggy (it doesn't have working hybrid_fragmented movflags). If you experience stutter in the mp4 file then update your FFmpeg to at least version 8 or record to a mkv file"); } } @@ -1036,7 +1037,7 @@ static RecordingStartResult start_recording_create_streams(const char *filename, const int open_ret = avio_open(&av_format_context->pb, filename, AVIO_FLAG_WRITE); if(open_ret < 0) { - fprintf(stderr, "gsr error: start_recording_create_streams: could not open '%s': %s\n", filename, av_error_to_string(open_ret)); + gsr_log(GSR_LOG_LEVEL_ERROR, "start_recording_create_streams: could not open '%s': %s", filename, av_error_to_string(open_ret)); return result; } @@ -1049,7 +1050,7 @@ static RecordingStartResult start_recording_create_streams(const char *filename, const int header_write_ret = avformat_write_header(av_format_context, &options); av_dict_free(&options); if(header_write_ret < 0) { - fprintf(stderr, "gsr error: start_recording_create_streams: error occurred when writing header to output file: %s\n", av_error_to_string(header_write_ret)); + gsr_log(GSR_LOG_LEVEL_ERROR, "start_recording_create_streams: error occurred when writing header to output file: %s", av_error_to_string(header_write_ret)); avio_close(av_format_context->pb); avformat_free_context(av_format_context); return result; @@ -1085,11 +1086,11 @@ static std::string create_new_recording_filepath_from_timestamp(std::string dire if(date_folders) { std::string output_folder = directory + '/' + get_date_only_str(); if(create_directory_recursive(&output_folder[0]) != 0) - fprintf(stderr, "gsr error: failed to create directory: %s\n", output_folder.c_str()); + gsr_log(GSR_LOG_LEVEL_ERROR, "failed to create directory: %s", output_folder.c_str()); output_filepath = output_folder + "/" + filename_prefix + "_" + get_time_only_str() + "." + file_extension; } else { if(create_directory_recursive(&directory[0]) != 0) - fprintf(stderr, "gsr error: failed to create directory: %s\n", directory.c_str()); + gsr_log(GSR_LOG_LEVEL_ERROR, "failed to create directory: %s", directory.c_str()); output_filepath = directory + "/" + filename_prefix + "_" + get_date_str() + "." + file_extension; } return output_filepath; @@ -1117,14 +1118,14 @@ static bool save_replay_async(AVCodecContext *video_codec_context, int video_str pthread_mutex_unlock(&encoder->replay_mutex); if(!cloned_replay_buffer) { // TODO: Return this error to mark the replay as failed - fprintf(stderr, "gsr error: failed to save replay: failed to clone replay buffer\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "failed to save replay: failed to clone replay buffer"); return false; } const gsr_replay_buffer_iterator search_start_iterator = current_save_replay_seconds == save_replay_seconds_full ? gsr_replay_buffer_iterator{0, 0} : gsr_replay_buffer_find_packet_index_by_time_passed(cloned_replay_buffer, current_save_replay_seconds); const gsr_replay_buffer_iterator video_start_iterator = gsr_replay_buffer_find_keyframe(cloned_replay_buffer, search_start_iterator, video_stream_index, false); if(video_start_iterator.packet_index == (size_t)-1) { - fprintf(stderr, "gsr error: failed to save replay: failed to find a video keyframe. perhaps replay was saved too fast, before anything has been recorded\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "failed to save replay: failed to find a video keyframe. perhaps replay was saved too fast, before anything has been recorded"); pthread_mutex_lock(&encoder->replay_mutex); gsr_replay_buffer_destroy(cloned_replay_buffer); pthread_mutex_unlock(&encoder->replay_mutex); @@ -1166,13 +1167,13 @@ static bool save_replay_async(AVCodecContext *video_codec_context, int video_str } if(!replay_packet) { - fprintf(stderr, "gsr error: save_replay_async: no replay packet\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "save_replay_async: no replay packet"); success = false; break; } if(!replay_packet->data && !replay_packet_data) { - fprintf(stderr, "gsr error: save_replay_async: no replay packet data\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "save_replay_async: no replay packet data"); success = false; break; } @@ -1198,7 +1199,7 @@ static bool save_replay_async(AVCodecContext *video_codec_context, int video_str } else { RecordingStartAudio *recording_start_audio = get_recording_start_item_by_stream_index(recording_start_result, av_packet.stream_index); if(!recording_start_audio) { - fprintf(stderr, "gsr error: save_replay_async: failed to find audio stream by index: %d\n", av_packet.stream_index); + gsr_log(GSR_LOG_LEVEL_ERROR, "save_replay_async: failed to find audio stream by index: %d", av_packet.stream_index); free(replay_packet_data); continue; } @@ -1218,7 +1219,7 @@ static bool save_replay_async(AVCodecContext *video_codec_context, int video_str const int ret = av_write_frame(recording_start_result.av_format_context, &av_packet); if(ret < 0) - fprintf(stderr, "gsr error: Failed to write frame index %d to muxer, reason: %s (%d)\n", av_packet.stream_index, av_error_to_string(ret), ret); + gsr_log(GSR_LOG_LEVEL_ERROR, "Failed to write frame index %d to muxer, reason: %s (%d)", av_packet.stream_index, av_error_to_string(ret), ret); free(replay_packet_data); @@ -1388,7 +1389,7 @@ static int init_filter_graph(AVCodecContext* audio_codec_context, AVFilterGraph* snprintf(args, sizeof(args), "inputs=%d:normalize=%s", (int)num_sources, normalize ? "true" : "false"); #else snprintf(args, sizeof(args), "inputs=%d", (int)num_sources); - fprintf(stderr, "gsr warning: your ffmpeg version doesn't support disabling normalizing of mixed audio. Volume might be lower than expected\n"); + gsr_log(GSR_LOG_LEVEL_WARNING, "your ffmpeg version doesn't support disabling normalizing of mixed audio. Volume might be lower than expected"); #endif err = avfilter_graph_create_filter(&mix_ctx, mix_filter, "amix", args, NULL, filter_graph); @@ -1807,7 +1808,7 @@ static WindowingSetup setup_windowing(bool setup_egl) { setup.dpy = XOpenDisplay(nullptr); if (!setup.dpy) { wayland = true; - fprintf(stderr, "gsr warning: failed to connect to the X server. Assuming wayland is running without Xwayland\n"); + gsr_log(GSR_LOG_LEVEL_WARNING, "failed to connect to the X server. Assuming wayland is running without Xwayland"); } XSetErrorHandler(x11_error_handler); @@ -1820,13 +1821,13 @@ static WindowingSetup setup_windowing(bool setup_egl) { // Disable prime-run and similar options as it doesn't work, the monitor to capture has to be run on the same device. // This is fine on wayland since nvidia uses drm interface there and the monitor query checks the monitors connected // to the drm device. - fprintf(stderr, "gsr warning: use of prime-run on X11 is not supported. Disabling prime-run\n"); + gsr_log(GSR_LOG_LEVEL_WARNING, "use of prime-run on X11 is not supported. Disabling prime-run"); disable_prime_run(); } setup.window = gsr_window_create(setup.dpy, wayland); if(!setup.window) { - fprintf(stderr, "gsr error: failed to create window\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "failed to create window"); _exit(1); } @@ -1834,7 +1835,7 @@ static WindowingSetup setup_windowing(bool setup_egl) { if(setup_egl) { if(!gsr_egl_load(&setup.egl, setup.window, false, false)) { - fprintf(stderr, "gsr error: failed to load opengl\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "failed to load opengl"); _exit(22); } @@ -1842,7 +1843,7 @@ static WindowingSetup setup_windowing(bool setup_egl) { if(monitor_capture_use_drm(setup.window, setup.egl.gpu_info.vendor)) { // TODO: Allow specifying another card, and in other places if(!gsr_get_valid_card_path(&setup.egl, setup.egl.card_path, true)) { - fprintf(stderr, "gsr error: no /dev/dri/cardX device found. Make sure that you have at least one monitor connected\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "no /dev/dri/cardX device found. Make sure that you have at least one monitor connected"); setup.list_monitors = false; } } else { @@ -1989,13 +1990,13 @@ static std::string validate_monitor_get_valid(const gsr_egl *egl, const char* wi capture_source_result = data.output_name; free(data.output_name); } else { - fprintf(stderr, "gsr error: no usable output found\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "no usable output found"); _exit(51); } } else if(capture_use_drm || (strcmp(capture_source_result.c_str(), "screen-direct") != 0 && strcmp(capture_source_result.c_str(), "screen-direct-force") != 0)) { gsr_monitor gmon; if(!get_monitor_by_name(egl, connection_type, capture_source_result.c_str(), &gmon)) { - fprintf(stderr, "gsr error: display \"%s\" not found, expected one of:\n", capture_source_result.c_str()); + gsr_log(GSR_LOG_LEVEL_ERROR, "display \"%s\" not found, expected one of:", capture_source_result.c_str()); fprintf(stderr, " \"screen\"\n"); if(!capture_use_drm) fprintf(stderr, " \"screen-direct\"\n"); @@ -2088,7 +2089,7 @@ static gsr_capture* create_monitor_capture(const args_parser &arg_parser, gsr_eg const bool direct_capture = strcmp(capture_source.name.c_str(), "screen-direct") == 0 || strcmp(capture_source.name.c_str(), "screen-direct-force") == 0; if(direct_capture) { capture_source_real = "screen"; - fprintf(stderr, "gsr warning: %s capture option is not recommended unless you use G-SYNC as Nvidia has driver issues that can cause your system or games to freeze/crash.\n", capture_source.name.c_str()); + gsr_log(GSR_LOG_LEVEL_WARNING, "%s capture option is not recommended unless you use G-SYNC as Nvidia has driver issues that can cause your system or games to freeze/crash.", capture_source.name.c_str()); } gsr_capture_nvfbc_params nvfbc_params; @@ -2119,7 +2120,7 @@ static std::string region_get_data(gsr_egl *egl, vec2i *region_size, vec2i *regi if(window.empty()) { const bool is_x11 = gsr_window_get_display_server(egl->window) == GSR_DISPLAY_SERVER_X11; const gsr_connection_type connection_type = is_x11 ? GSR_CONNECTION_X11 : GSR_CONNECTION_WAYLAND; - fprintf(stderr, "gsr error: the region %dx%d+%d+%d doesn't match any monitor. Available monitors and their regions:\n", region_size->x, region_size->y, region_position->x, region_position->y); + gsr_log(GSR_LOG_LEVEL_ERROR, "the region %dx%d+%d+%d doesn't match any monitor. Available monitors and their regions:", region_size->x, region_size->y, region_position->x, region_position->y); MonitorOutputCallbackUserdata userdata; userdata.window = egl->window; @@ -2151,12 +2152,12 @@ static gsr_capture* create_capture_impl(const args_parser &arg_parser, gsr_egl * gsr_capture *capture = nullptr; if(capture_source.type == GSR_CAPTURE_SOURCE_TYPE_FOCUSED_WINDOW) { if(wayland) { - fprintf(stderr, "gsr error: GPU Screen Recorder window capture only works in a pure X11 session. Xwayland is not supported. You can record a monitor instead on wayland\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "GPU Screen Recorder window capture only works in a pure X11 session. Xwayland is not supported. You can record a monitor instead on wayland"); _exit(2); } if(arg_parser.output_resolution.x <= 0 || arg_parser.output_resolution.y <= 0) { - fprintf(stderr, "gsr error: invalid value for option -s '%dx%d' when using -w focused option. expected width and height to be greater than 0\n", arg_parser.output_resolution.x, arg_parser.output_resolution.y); + gsr_log(GSR_LOG_LEVEL_ERROR, "invalid value for option -s '%dx%d' when using -w focused option. expected width and height to be greater than 0", arg_parser.output_resolution.x, arg_parser.output_resolution.y); args_parser_print_usage(); _exit(1); } @@ -2166,7 +2167,7 @@ static gsr_capture* create_capture_impl(const args_parser &arg_parser, gsr_egl * #ifdef GSR_PORTAL // Desktop portal capture on x11 doesn't seem to be hardware accelerated if(!wayland) { - fprintf(stderr, "gsr error: desktop portal capture is not supported on X11\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "desktop portal capture is not supported on X11"); _exit(1); } @@ -2181,7 +2182,7 @@ static gsr_capture* create_capture_impl(const args_parser &arg_parser, gsr_egl * if(!capture) _exit(1); #else - fprintf(stderr, "gsr error: option '-w portal' used but GPU Screen Recorder was compiled without desktop portal support. Please recompile GPU Screen recorder with the -Dportal=true option\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "option '-w portal' used but GPU Screen Recorder was compiled without desktop portal support. Please recompile GPU Screen recorder with the -Dportal=true option"); _exit(2); #endif } else if(capture_source.type == GSR_CAPTURE_SOURCE_TYPE_REGION) { @@ -2209,7 +2210,7 @@ static gsr_capture* create_capture_impl(const args_parser &arg_parser, gsr_egl * _exit(1); } else { if(wayland) { - fprintf(stderr, "gsr error: GPU Screen Recorder window capture only works in a pure X11 session. Xwayland is not supported. You can record a monitor instead on wayland or use -w portal option which supports window capture if your wayland compositor supports window capture\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "GPU Screen Recorder window capture only works in a pure X11 session. Xwayland is not supported. You can record a monitor instead on wayland or use -w portal option which supports window capture if your wayland compositor supports window capture"); _exit(2); } } @@ -2280,7 +2281,7 @@ static std::vector create_video_sources(const args_parser &arg_pars for(VideoSource &video_source : video_sources) { int capture_result = gsr_capture_start(video_source.capture, &video_source.metadata); if(capture_result != 0) { - fprintf(stderr, "gsr error: gsr_capture_start failed\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_capture_start failed"); _exit(capture_result); } } @@ -2411,7 +2412,7 @@ static void capture_image_to_file(args_parser &arg_parser, gsr_egl *egl, gsr_win gsr_image_writer image_writer; if(!gsr_image_writer_init_opengl(&image_writer, egl, video_size.x, video_size.y)) { - fprintf(stderr, "gsr error: capture_image_to_file: gsr_image_write_gl_init failed\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "capture_image_to_file: gsr_image_write_gl_init failed"); _exit(1); } @@ -2428,7 +2429,7 @@ static void capture_image_to_file(args_parser &arg_parser, gsr_egl *egl, gsr_win gsr_color_conversion color_conversion; if(gsr_color_conversion_init(&color_conversion, &color_conversion_params) != 0) { - fprintf(stderr, "gsr error: capture_image_to_file: failed to create color conversion\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "capture_image_to_file: failed to create color conversion"); _exit(1); } @@ -2456,7 +2457,7 @@ static void capture_image_to_file(args_parser &arg_parser, gsr_egl *egl, gsr_win if(kms_client_initialized) { if(gsr_kms_client_get_kms(&kms_client, &kms_response) != 0) - fprintf(stderr, "gsr error: failed to get kms, error: %d (%s)\n", kms_response.result, kms_response.err_msg); + gsr_log(GSR_LOG_LEVEL_ERROR, "failed to get kms, error: %d (%s)", kms_response.result, kms_response.err_msg); } should_stop_error = false; @@ -2507,7 +2508,7 @@ static void capture_image_to_file(args_parser &arg_parser, gsr_egl *egl, gsr_win if(!should_stop_error) { if(!gsr_image_writer_write_to_file(&image_writer, arg_parser.filename, image_format, image_quality)) { - fprintf(stderr, "gsr error: capture_image_to_file: failed to write opengl texture to image output file %s\n", arg_parser.filename); + gsr_log(GSR_LOG_LEVEL_ERROR, "capture_image_to_file: failed to write opengl texture to image output file %s", arg_parser.filename); _exit(1); } @@ -2548,7 +2549,7 @@ static void match_app_audio_input_to_available_apps(const std::vector parse_audio_inputs(const AudioDevices &aud if(request_audio_input.name == "default_output") { if(audio_devices.default_output.empty()) { - fprintf(stderr, "gsr error: -a default_output was specified but no default audio output is specified in the audio server\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "-a default_output was specified but no default audio output is specified in the audio server"); _exit(2); } match = true; audio_track_description.devices.push_back("Default output"); } else if(request_audio_input.name == "default_input") { if(audio_devices.default_input.empty()) { - fprintf(stderr, "gsr error: -a default_input was specified but no default audio input is specified in the audio server\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "-a default_input was specified but no default audio input is specified in the audio server"); _exit(2); } match = true; @@ -2646,7 +2647,7 @@ static std::vector parse_audio_inputs(const AudioDevices &aud } if(!match) { - fprintf(stderr, "gsr error: Audio device '%s' is not a valid audio device, expected one of:\n", request_audio_input.name.c_str()); + gsr_log(GSR_LOG_LEVEL_ERROR, "Audio device '%s' is not a valid audio device, expected one of:", request_audio_input.name.c_str()); if(!audio_devices.default_output.empty()) fprintf(stderr, " default_output (Default output)\n"); if(!audio_devices.default_input.empty()) @@ -2783,7 +2784,7 @@ static void parse_capture_source_options(const std::string &capture_source_str, sub += 2; size -= 2; if(!string_to_int(sub, size, &capture_source.pos.x)) { - fprintf(stderr, "gsr error: invalid capture target value for option x: \"%.*s\", expected a number\n", (int)size, sub); + gsr_log(GSR_LOG_LEVEL_ERROR, "invalid capture target value for option x: \"%.*s\", expected a number", (int)size, sub); _exit(1); } } else if(string_starts_with(sub, size, "y=")) { @@ -2791,7 +2792,7 @@ static void parse_capture_source_options(const std::string &capture_source_str, sub += 2; size -= 2; if(!string_to_int(sub, size, &capture_source.pos.y)) { - fprintf(stderr, "gsr error: invalid capture target value for option y: \"%.*s\", expected a number\n", (int)size, sub); + gsr_log(GSR_LOG_LEVEL_ERROR, "invalid capture target value for option y: \"%.*s\", expected a number", (int)size, sub); _exit(1); } @@ -2800,7 +2801,7 @@ static void parse_capture_source_options(const std::string &capture_source_str, sub += 6; size -= 6; if(!string_to_int(sub, size, &capture_source.size.x)) { - fprintf(stderr, "gsr error: invalid capture target value for option width: \"%.*s\", expected a number\n", (int)size, sub); + gsr_log(GSR_LOG_LEVEL_ERROR, "invalid capture target value for option width: \"%.*s\", expected a number", (int)size, sub); _exit(1); } } else if(string_starts_with(sub, size, "height=")) { @@ -2808,28 +2809,28 @@ static void parse_capture_source_options(const std::string &capture_source_str, sub += 7; size -= 7; if(!string_to_int(sub, size, &capture_source.size.y)) { - fprintf(stderr, "gsr error: invalid capture target value for option height: \"%.*s\", expected a number\n", (int)size, sub); + gsr_log(GSR_LOG_LEVEL_ERROR, "invalid capture target value for option height: \"%.*s\", expected a number", (int)size, sub); _exit(1); } } else if(string_starts_with(sub, size, "halign=")) { sub += 7; size -= 7; if(!string_to_capture_alignment(sub, size, &capture_source.halign)) { - fprintf(stderr, "gsr error: invalid capture target value for option halign: \"%.*s\", expected a \"start\", \"center\" or \"end\"\n", (int)size, sub); + gsr_log(GSR_LOG_LEVEL_ERROR, "invalid capture target value for option halign: \"%.*s\", expected a \"start\", \"center\" or \"end\"", (int)size, sub); _exit(1); } } else if(string_starts_with(sub, size, "valign=")) { sub += 7; size -= 7; if(!string_to_capture_alignment(sub, size, &capture_source.valign)) { - fprintf(stderr, "gsr error: invalid capture target value for option valign: \"%.*s\", expected a \"start\", \"center\" or \"end\"\n", (int)size, sub); + gsr_log(GSR_LOG_LEVEL_ERROR, "invalid capture target value for option valign: \"%.*s\", expected a \"start\", \"center\" or \"end\"", (int)size, sub); _exit(1); } } else if(string_starts_with(sub, size, "pixfmt=")) { sub += 7; size -= 7; if(!string_to_v4l2_pixfmt(sub, size, &capture_source.v4l2_pixfmt)) { - fprintf(stderr, "gsr error: invalid v4l2 pixfmt value for option pixfmt: \"%.*s\", expected a \"auto\", \"yuyv\" or \"mjpeg\"\n", (int)size, sub); + gsr_log(GSR_LOG_LEVEL_ERROR, "invalid v4l2 pixfmt value for option pixfmt: \"%.*s\", expected a \"auto\", \"yuyv\" or \"mjpeg\"", (int)size, sub); _exit(1); } } else if(string_starts_with(sub, size, "hflip=")) { @@ -2837,7 +2838,7 @@ static void parse_capture_source_options(const std::string &capture_source_str, size -= 6; bool hflip = false; if(!string_to_bool(sub, size, &hflip)) { - fprintf(stderr, "gsr error: invalid bool value for option hflip: \"%.*s\", expected a \"true\" or \"false\"\n", (int)size, sub); + gsr_log(GSR_LOG_LEVEL_ERROR, "invalid bool value for option hflip: \"%.*s\", expected a \"true\" or \"false\"", (int)size, sub); _exit(1); } @@ -2848,7 +2849,7 @@ static void parse_capture_source_options(const std::string &capture_source_str, size -= 6; bool vflip = false; if(!string_to_bool(sub, size, &vflip)) { - fprintf(stderr, "gsr error: invalid bool value for option vflip: \"%.*s\", expected a \"true\" or \"false\"\n", (int)size, sub); + gsr_log(GSR_LOG_LEVEL_ERROR, "invalid bool value for option vflip: \"%.*s\", expected a \"true\" or \"false\"", (int)size, sub); _exit(1); } @@ -2858,25 +2859,25 @@ static void parse_capture_source_options(const std::string &capture_source_str, sub += 11; size -= 11; if(!string_to_int(sub, size, &capture_source.camera_fps)) { - fprintf(stderr, "gsr error: invalid capture target value for option camera_fps: \"%.*s\", expected a number\n", (int)size, sub); + gsr_log(GSR_LOG_LEVEL_ERROR, "invalid capture target value for option camera_fps: \"%.*s\", expected a number", (int)size, sub); _exit(1); } } else if(string_starts_with(sub, size, "camera_width=")) { sub += 13; size -= 13; if(!string_to_int(sub, size, &capture_source.camera_resolution.x)) { - fprintf(stderr, "gsr error: invalid capture target value for option camera_width: \"%.*s\", expected a number\n", (int)size, sub); + gsr_log(GSR_LOG_LEVEL_ERROR, "invalid capture target value for option camera_width: \"%.*s\", expected a number", (int)size, sub); _exit(1); } } else if(string_starts_with(sub, size, "camera_height=")) { sub += 14; size -= 14; if(!string_to_int(sub, size, &capture_source.camera_resolution.y)) { - fprintf(stderr, "gsr error: invalid capture target value for option camera_height: \"%.*s\", expected a number\n", (int)size, sub); + gsr_log(GSR_LOG_LEVEL_ERROR, "invalid capture target value for option camera_height: \"%.*s\", expected a number", (int)size, sub); _exit(1); } } else { - fprintf(stderr, "gsr error: invalid capture target option \"%.*s\", expected x, y, width, height, halign, valign, pixfmt, hflip, vflip, camera_fps, camera_width or camera_height\n", (int)size, sub); + gsr_log(GSR_LOG_LEVEL_ERROR, "invalid capture target option \"%.*s\", expected x, y, width, height, halign, valign, pixfmt, hflip, vflip, camera_fps, camera_width or camera_height", (int)size, sub); _exit(1); } @@ -2922,7 +2923,7 @@ static std::vector parse_capture_source_arg(const char *capture_s if(capture_source.type == GSR_CAPTURE_SOURCE_TYPE_WINDOW) { if(!string_to_int(capture_source.name.c_str(), capture_source.name.size(), &capture_source.window_id)) { - fprintf(stderr, "gsr error: invalid window number %s\n", capture_source.name.c_str()); + gsr_log(GSR_LOG_LEVEL_ERROR, "invalid window number %s", capture_source.name.c_str()); args_parser_print_usage(); _exit(1); } @@ -2998,7 +2999,7 @@ static void validate_merged_audio_inputs_app_audio(const std::vector 0 && num_app_inverted_audio > 0) { - fprintf(stderr, "gsr error: argument -a was provided with both app: and app-inverse:, only one of them can be used for one audio track\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "argument -a was provided with both app: and app-inverse:, only one of them can be used for one audio track"); _exit(2); } } @@ -3010,7 +3011,7 @@ static gsr_audio_codec select_audio_codec_with_fallback(gsr_audio_codec audio_co if(file_extension == "webm") { //audio_codec_to_use = "opus"; audio_codec = GSR_AUDIO_CODEC_OPUS; - fprintf(stderr, "gsr warning: .webm files only support opus audio codec, changing audio codec from aac to opus\n"); + gsr_log(GSR_LOG_LEVEL_WARNING, ".webm files only support opus audio codec, changing audio codec from aac to opus"); } break; } @@ -3018,7 +3019,7 @@ static gsr_audio_codec select_audio_codec_with_fallback(gsr_audio_codec audio_co if(file_extension != "mp4" && file_extension != "mkv" && file_extension != "webm" && file_extension != "ts" && file_extension != "whip") { //audio_codec_to_use = "aac"; audio_codec = GSR_AUDIO_CODEC_AAC; - fprintf(stderr, "gsr warning: opus audio codec is only supported by .mp4, .mkv, .webm and .ts files, falling back to aac instead\n"); + gsr_log(GSR_LOG_LEVEL_WARNING, "opus audio codec is only supported by .mp4, .mkv, .webm and .ts files, falling back to aac instead"); } break; } @@ -3027,16 +3028,16 @@ static gsr_audio_codec select_audio_codec_with_fallback(gsr_audio_codec audio_co if(file_extension == "webm") { //audio_codec_to_use = "opus"; audio_codec = GSR_AUDIO_CODEC_OPUS; - fprintf(stderr, "gsr warning: .webm files only support opus audio codec, changing audio codec from flac to opus\n"); + gsr_log(GSR_LOG_LEVEL_WARNING, ".webm files only support opus audio codec, changing audio codec from flac to opus"); } else if(file_extension != "mp4" && file_extension != "mkv") { //audio_codec_to_use = "aac"; audio_codec = GSR_AUDIO_CODEC_AAC; - fprintf(stderr, "gsr warning: flac audio codec is only supported by .mp4 and .mkv files, falling back to aac instead\n"); + gsr_log(GSR_LOG_LEVEL_WARNING, "flac audio codec is only supported by .mp4 and .mkv files, falling back to aac instead"); } else if(uses_amix) { // TODO: remove this? is it true anymore? //audio_codec_to_use = "opus"; audio_codec = GSR_AUDIO_CODEC_OPUS; - fprintf(stderr, "gsr warning: flac audio codec is not supported when mixing audio sources, falling back to opus instead\n"); + gsr_log(GSR_LOG_LEVEL_WARNING, "flac audio codec is not supported when mixing audio sources, falling back to opus instead"); } break; } @@ -3197,21 +3198,14 @@ static void print_codec_error(gsr_video_codec video_codec) { video_codec = GSR_VIDEO_CODEC_H264; const char *video_codec_name = video_codec_to_string(video_codec); - fprintf(stderr, "gsr error: your gpu does not support '%s' video codec. If you are sure that your gpu does support '%s' video encoding and you are using an AMD/Intel GPU,\n" - " then make sure you have installed the GPU specific vaapi packages (intel-media-driver, libva-intel-driver, libva-mesa-driver and linux-firmware).\n" - " It's also possible that your distro has disabled hardware accelerated video encoding for '%s' video codec.\n" - " This may be the case on corporate distros such as Manjaro, Fedora or OpenSUSE.\n" - " You can test this by running 'vainfo | grep VAEntrypointEncSlice' to see if it matches any H264/HEVC/AV1/VP8/VP9 profile.\n" - " On such distros, you need to manually install mesa from source to enable H264/HEVC hardware acceleration, or use a more user friendly distro. Alternatively record with AV1 if supported by your GPU.\n" - " You can alternatively use the flatpak version of GPU Screen Recorder (https://flathub.org/apps/com.dec05eba.gpu_screen_recorder) which bypasses system issues with patented H264/HEVC codecs.\n" - " If your GPU doesn't support hardware accelerated video encoding then you can use '-fallback-cpu-encoding yes' option to encode with your cpu instead.\n", video_codec_name, video_codec_name, video_codec_name); + gsr_log(GSR_LOG_LEVEL_ERROR, "your gpu does not support '%s' video codec. If you are sure that your gpu does support '%s' video encoding and you are using an AMD/Intel GPU,\n then make sure you have installed the GPU specific vaapi packages (intel-media-driver, libva-intel-driver, libva-mesa-driver and linux-firmware).\n It's also possible that your distro has disabled hardware accelerated video encoding for '%s' video codec.\n This may be the case on corporate distros such as Manjaro, Fedora or OpenSUSE.\n You can test this by running 'vainfo | grep VAEntrypointEncSlice' to see if it matches any H264/HEVC/AV1/VP8/VP9 profile.\n On such distros, you need to manually install mesa from source to enable H264/HEVC hardware acceleration, or use a more user friendly distro. Alternatively record with AV1 if supported by your GPU.\n You can alternatively use the flatpak version of GPU Screen Recorder (https://flathub.org/apps/com.dec05eba.gpu_screen_recorder) which bypasses system issues with patented H264/HEVC codecs.\n If your GPU doesn't support hardware accelerated video encoding then you can use '-fallback-cpu-encoding yes' option to encode with your cpu instead.", video_codec_name, video_codec_name, video_codec_name); } static void force_cpu_encoding(args_parser *args_parser) { args_parser->video_codec = GSR_VIDEO_CODEC_H264; args_parser->video_encoder = GSR_VIDEO_ENCODER_HW_CPU; if(args_parser->bitrate_mode == GSR_BITRATE_MODE_VBR) { - fprintf(stderr, "gsr warning: bitrate mode has been forcefully set to qp because software encoding option doesn't support vbr option\n"); + gsr_log(GSR_LOG_LEVEL_WARNING, "bitrate mode has been forcefully set to qp because software encoding option doesn't support vbr option"); args_parser->bitrate_mode = GSR_BITRATE_MODE_QP; } } @@ -3224,9 +3218,9 @@ static const AVCodec* pick_video_codec(gsr_egl *egl, args_parser *args_parser, b if(!video_codec_f && use_fallback_codec && args_parser->video_encoder != GSR_VIDEO_ENCODER_HW_CPU) { switch(args_parser->video_codec) { case GSR_VIDEO_CODEC_H264: { - fprintf(stderr, "gsr error: selected video codec h264 is not supported by your hardware\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "selected video codec h264 is not supported by your hardware"); if(args_parser->fallback_cpu_encoding) { - fprintf(stderr, "gsr warning: gpu encoding is not available on your system, trying cpu encoding instead because -fallback-cpu-encoding is enabled. Install the proper vaapi drivers on your system (if supported) if you experience performance issues\n"); + gsr_log(GSR_LOG_LEVEL_WARNING, "gpu encoding is not available on your system, trying cpu encoding instead because -fallback-cpu-encoding is enabled. Install the proper vaapi drivers on your system (if supported) if you experience performance issues"); force_cpu_encoding(args_parser); } break; @@ -3234,14 +3228,14 @@ static const AVCodec* pick_video_codec(gsr_egl *egl, args_parser *args_parser, b case GSR_VIDEO_CODEC_HEVC: case GSR_VIDEO_CODEC_HEVC_HDR: case GSR_VIDEO_CODEC_HEVC_10BIT: { - fprintf(stderr, "gsr warning: selected video codec hevc is not supported by your hardware, trying h264 instead\n"); + gsr_log(GSR_LOG_LEVEL_WARNING, "selected video codec hevc is not supported by your hardware, trying h264 instead"); args_parser->video_codec = GSR_VIDEO_CODEC_H264; return pick_video_codec(egl, args_parser, true, low_power, supported_video_codecs); } case GSR_VIDEO_CODEC_AV1: case GSR_VIDEO_CODEC_AV1_HDR: case GSR_VIDEO_CODEC_AV1_10BIT: { - fprintf(stderr, "gsr warning: selected video codec av1 is not supported by your hardware, trying h264 instead\n"); + gsr_log(GSR_LOG_LEVEL_WARNING, "selected video codec av1 is not supported by your hardware, trying h264 instead"); args_parser->video_codec = GSR_VIDEO_CODEC_H264; return pick_video_codec(egl, args_parser, true, low_power, supported_video_codecs); } @@ -3250,11 +3244,11 @@ static const AVCodec* pick_video_codec(gsr_egl *egl, args_parser *args_parser, b // TODO: Cant fallback to other codec because webm only supports vp8/vp9 break; case GSR_VIDEO_CODEC_H264_VULKAN: { - fprintf(stderr, "gsr warning: selected video codec h264_vulkan is not supported by your hardware, trying h264 instead\n"); + gsr_log(GSR_LOG_LEVEL_WARNING, "selected video codec h264_vulkan is not supported by your hardware, trying h264 instead"); args_parser->video_codec = GSR_VIDEO_CODEC_H264; // Need to do a query again because this time it's without vulkan if(!get_supported_video_codecs(egl, args_parser->video_codec, false, true, supported_video_codecs)) { - fprintf(stderr, "gsr error: failed to query for supported video codecs\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "failed to query for supported video codecs"); print_codec_error(args_parser->video_codec); _exit(11); } @@ -3263,11 +3257,11 @@ static const AVCodec* pick_video_codec(gsr_egl *egl, args_parser *args_parser, b case GSR_VIDEO_CODEC_HEVC_VULKAN: case GSR_VIDEO_CODEC_HEVC_HDR_VULKAN: case GSR_VIDEO_CODEC_HEVC_10BIT_VULKAN: { - fprintf(stderr, "gsr warning: selected video codec hevc_vulkan is not supported by your hardware, trying hevc instead\n"); + gsr_log(GSR_LOG_LEVEL_WARNING, "selected video codec hevc_vulkan is not supported by your hardware, trying hevc instead"); args_parser->video_codec = GSR_VIDEO_CODEC_HEVC; // Need to do a query again because this time it's without vulkan if(!get_supported_video_codecs(egl, args_parser->video_codec, false, true, supported_video_codecs)) { - fprintf(stderr, "gsr error: failed to query for supported video codecs\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "failed to query for supported video codecs"); print_codec_error(args_parser->video_codec); _exit(11); } @@ -3276,11 +3270,11 @@ static const AVCodec* pick_video_codec(gsr_egl *egl, args_parser *args_parser, b case GSR_VIDEO_CODEC_AV1_VULKAN: case GSR_VIDEO_CODEC_AV1_HDR_VULKAN: case GSR_VIDEO_CODEC_AV1_10BIT_VULKAN: { - fprintf(stderr, "gsr warning: selected video codec av1_vulkan is not supported by your hardware, trying av1 instead\n"); + gsr_log(GSR_LOG_LEVEL_WARNING, "selected video codec av1_vulkan is not supported by your hardware, trying av1 instead"); args_parser->video_codec = GSR_VIDEO_CODEC_AV1; // Need to do a query again because this time it's without vulkan if(!get_supported_video_codecs(egl, args_parser->video_codec, false, true, supported_video_codecs)) { - fprintf(stderr, "gsr error: failed to query for supported video codecs\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "failed to query for supported video codecs"); print_codec_error(args_parser->video_codec); _exit(11); } @@ -3304,16 +3298,14 @@ static const AVCodec* pick_video_codec(gsr_egl *egl, args_parser *args_parser, b /* Returns -1 if none is available */ static gsr_video_codec select_appropriate_video_codec_automatically(vec2i video_size, const gsr_supported_video_codecs *supported_video_codecs) { if(supported_video_codecs->h264.supported && codec_supports_resolution(supported_video_codecs->h264.max_resolution, video_size)) { - fprintf(stderr, "gsr info: using h264 encoder because a codec was not specified\n"); + gsr_log(GSR_LOG_LEVEL_INFO, "using h264 encoder because a codec was not specified"); return GSR_VIDEO_CODEC_H264; } else if(supported_video_codecs->hevc.supported && codec_supports_resolution(supported_video_codecs->hevc.max_resolution, video_size)) { - fprintf(stderr, "gsr info: using hevc encoder because a codec was not specified and h264 supported max resolution (%dx%d) is less than the capture resolution (%dx%d)\n", - supported_video_codecs->h264.max_resolution.x, supported_video_codecs->h264.max_resolution.y, + gsr_log(GSR_LOG_LEVEL_INFO, "using hevc encoder because a codec was not specified and h264 supported max resolution (%dx%d) is less than the capture resolution (%dx%d)", supported_video_codecs->h264.max_resolution.x, supported_video_codecs->h264.max_resolution.y, video_size.x, video_size.y); return GSR_VIDEO_CODEC_HEVC; } else if(supported_video_codecs->av1.supported && codec_supports_resolution(supported_video_codecs->av1.max_resolution, video_size)) { - fprintf(stderr, "gsr info: using av1 encoder because a codec was not specified and hevc supported max resolution (%dx%d) is less than the capture resolution (%dx%d)\n", - supported_video_codecs->hevc.max_resolution.x, supported_video_codecs->hevc.max_resolution.y, + gsr_log(GSR_LOG_LEVEL_INFO, "using av1 encoder because a codec was not specified and hevc supported max resolution (%dx%d) is less than the capture resolution (%dx%d)", supported_video_codecs->hevc.max_resolution.x, supported_video_codecs->hevc.max_resolution.y, video_size.x, video_size.y); return GSR_VIDEO_CODEC_AV1; } else { @@ -3335,19 +3327,19 @@ static const AVCodec* select_video_codec_with_fallback(vec2i video_size, args_pa const bool video_codec_auto = args_parser->video_codec == (gsr_video_codec)GSR_VIDEO_CODEC_AUTO; if(video_codec_auto) { if(strcmp(file_extension, "webm") == 0) { - fprintf(stderr, "gsr info: using vp8 encoder because a codec was not specified and the file extension is .webm\n"); + gsr_log(GSR_LOG_LEVEL_INFO, "using vp8 encoder because a codec was not specified and the file extension is .webm"); args_parser->video_codec = GSR_VIDEO_CODEC_VP8; } else if(args_parser->video_encoder == GSR_VIDEO_ENCODER_HW_CPU) { - fprintf(stderr, "gsr info: using h264 encoder because a codec was not specified\n"); + gsr_log(GSR_LOG_LEVEL_INFO, "using h264 encoder because a codec was not specified"); args_parser->video_codec = GSR_VIDEO_CODEC_H264; } else if(args_parser->video_encoder != GSR_VIDEO_ENCODER_HW_CPU) { args_parser->video_codec = select_appropriate_video_codec_automatically(video_size, &supported_video_codecs_non_vulkan); if(args_parser->video_codec == (gsr_video_codec)-1) { if(args_parser->fallback_cpu_encoding) { - fprintf(stderr, "gsr warning: gpu encoding is not available on your system or your gpu doesn't support recording at the resolution you are trying to record, trying cpu encoding instead because -fallback-cpu-encoding is enabled. Install the proper vaapi drivers on your system (if supported) if you experience performance issues\n"); + gsr_log(GSR_LOG_LEVEL_WARNING, "gpu encoding is not available on your system or your gpu doesn't support recording at the resolution you are trying to record, trying cpu encoding instead because -fallback-cpu-encoding is enabled. Install the proper vaapi drivers on your system (if supported) if you experience performance issues"); force_cpu_encoding(args_parser); } else { - fprintf(stderr, "gsr error: no video encoder was specified and neither h264, hevc nor av1 are supported on your system or you are trying to capture at a resolution higher than your system supports for each codec.\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "no video encoder was specified and neither h264, hevc nor av1 are supported on your system or you are trying to capture at a resolution higher than your system supports for each codec."); fprintf(stderr, " Ensure that you have installed the proper vaapi driver. If your gpu doesn't support video encoding then you can run gpu-screen-recorder with \"-fallback-cpu-encoding yes\" option to use cpu encoding.\n"); _exit(52); } @@ -3358,12 +3350,12 @@ static const AVCodec* select_video_codec_with_fallback(vec2i video_size, args_pa if(LIBAVFORMAT_VERSION_INT < AV_VERSION_INT(60, 10, 100) && strcmp(file_extension, "flv") == 0) { if(args_parser->video_codec != GSR_VIDEO_CODEC_H264) { args_parser->video_codec = GSR_VIDEO_CODEC_H264; - fprintf(stderr, "gsr warning: hevc/av1 is not compatible with flv in your outdated version of ffmpeg, falling back to h264 instead.\n"); + gsr_log(GSR_LOG_LEVEL_WARNING, "hevc/av1 is not compatible with flv in your outdated version of ffmpeg, falling back to h264 instead."); } } else if(strcmp(file_extension, "m3u8") == 0) { if(video_codec_is_av1(args_parser->video_codec)) { args_parser->video_codec = GSR_VIDEO_CODEC_HEVC; - fprintf(stderr, "gsr warning: av1 is not compatible with hls (m3u8), falling back to hevc instead.\n"); + gsr_log(GSR_LOG_LEVEL_WARNING, "av1 is not compatible with hls (m3u8), falling back to hevc instead."); } } @@ -3372,8 +3364,7 @@ static const AVCodec* select_video_codec_with_fallback(vec2i video_size, args_pa const vec2i codec_max_resolution = codec_get_max_resolution(args_parser->video_codec, args_parser->video_encoder == GSR_VIDEO_ENCODER_HW_CPU, supported_video_codecs); if(!codec_supports_resolution(codec_max_resolution, video_size)) { const char *video_codec_name = video_codec_to_string(args_parser->video_codec); - fprintf(stderr, "gsr error: The max resolution for video codec %s is %dx%d while you are trying to capture at resolution %dx%d. Change capture resolution or video codec and try again\n", - video_codec_name, codec_max_resolution.x, codec_max_resolution.y, video_size.x, video_size.y); + gsr_log(GSR_LOG_LEVEL_ERROR, "The max resolution for video codec %s is %dx%d while you are trying to capture at resolution %dx%d. Change capture resolution or video codec and try again", video_codec_name, codec_max_resolution.x, codec_max_resolution.y, video_size.x, video_size.y); _exit(53); } @@ -3398,7 +3389,7 @@ static std::vector create_device_audio_inputs(const std::vector } else { const std::string description = "gsr-" + audio_input.name; if(sound_device_get_by_name(&audio_device.sound_device, description.c_str(), audio_input.name.c_str(), description.c_str(), num_channels, audio_codec_context->frame_size, audio_codec_context_get_audio_format(audio_codec_context)) != 0) { - fprintf(stderr, "gsr error: failed to get \"%s\" audio device\n", audio_input.name.c_str()); + gsr_log(GSR_LOG_LEVEL_ERROR, "failed to get \"%s\" audio device", audio_input.name.c_str()); _exit(1); } } @@ -3419,7 +3410,7 @@ static AudioDeviceData create_application_audio_audio_input(const MergedAudioInp char random_str[8]; if(!generate_random_characters_standard_alphabet(random_str, sizeof(random_str))) { - fprintf(stderr, "gsr error: failed to generate random string\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "failed to generate random string"); _exit(1); } @@ -3428,7 +3419,7 @@ static AudioDeviceData create_application_audio_audio_input(const MergedAudioInp combined_sink_name += ".monitor"; if(sound_device_get_by_name(&audio_device.sound_device, combined_sink_name.c_str(), "", "gpu-screen-recorder", num_channels, audio_codec_context->frame_size, audio_codec_context_get_audio_format(audio_codec_context)) != 0) { - fprintf(stderr, "gsr error: failed to setup audio recording to combined sink\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "failed to setup audio recording to combined sink"); _exit(1); } @@ -3449,19 +3440,19 @@ static AudioDeviceData create_application_audio_audio_input(const MergedAudioInp if(!audio_devices_sources.empty()) { if(!gsr_pipewire_audio_add_link_from_sources_to_stream(pipewire_audio, audio_devices_sources.data(), audio_devices_sources.size(), combined_sink_name.c_str())) { - fprintf(stderr, "gsr error: failed to add application audio link\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "failed to add application audio link"); _exit(1); } } if(app_audio_inverted) { if(!gsr_pipewire_audio_add_link_from_apps_to_stream_inverted(pipewire_audio, app_names.data(), app_names.size(), combined_sink_name.c_str())) { - fprintf(stderr, "gsr error: failed to add application audio link\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "failed to add application audio link"); _exit(1); } } else { if(!gsr_pipewire_audio_add_link_from_apps_to_stream(pipewire_audio, app_names.data(), app_names.size(), combined_sink_name.c_str())) { - fprintf(stderr, "gsr error: failed to add application audio link\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "failed to add application audio link"); _exit(1); } } @@ -3574,7 +3565,7 @@ static void validate_args_with_capture_sources(args_parser &arg_parser, const st assert(region_arg); if(is_capturing_type(capture_sources, GSR_CAPTURE_SOURCE_TYPE_FOCUSED_WINDOW) && output_resolution_arg->num_values == 0) { - fprintf(stderr, "gsr error: option -s is required when using '-w focused' option\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "option -s is required when using '-w focused' option"); args_parser_print_usage(); _exit(1); } @@ -3582,22 +3573,22 @@ static void validate_args_with_capture_sources(args_parser &arg_parser, const st const bool is_capturing_region = is_capturing_type(capture_sources, GSR_CAPTURE_SOURCE_TYPE_REGION); if(region_arg->num_values == 0) { if(is_capturing_region && !has_capture_source_with_region_set(capture_sources)) { - fprintf(stderr, "gsr error: option -region is required when '-w region' is used\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "option -region is required when '-w region' is used"); args_parser_print_usage(); _exit(1); } } else { if(is_capturing_region) { - fprintf(stderr, "gsr warning: option -region is deprecated, use -w with region directly instead, for example: -w %s\n", region_arg->values[0]); + 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 { - fprintf(stderr, "gsr error: option -region can only be used when option '-w region' is used\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "option -region can only be used when option '-w region' is used"); args_parser_print_usage(); _exit(1); } } if(!arg_parser.restore_portal_session && is_capturing_type(capture_sources, GSR_CAPTURE_SOURCE_TYPE_PORTAL)) - fprintf(stderr, "gsr info: option '-w portal' was used without '-restore-portal-session yes'. The previous screencast session will be ignored\n"); + gsr_log(GSR_LOG_LEVEL_INFO, "option '-w portal' was used without '-restore-portal-session yes'. The previous screencast session will be ignored"); } static void install_cuda_no_stable_perf_limit() { @@ -3606,7 +3597,7 @@ static void install_cuda_no_stable_perf_limit() { const char *home = getenv("HOME"); if(!home) { - fprintf(stderr, "gsr warning: install_cuda_no_stable_perf_limit: $HOME not set\n"); + gsr_log(GSR_LOG_LEVEL_WARNING, "install_cuda_no_stable_perf_limit: $HOME not set"); return; } @@ -3614,7 +3605,7 @@ static void install_cuda_no_stable_perf_limit() { snprintf(nv_profiles_path, sizeof(nv_profiles_path), "%s/.nv/nvidia-application-profiles-rc.d", home); if(create_directory_recursive(nv_profiles_path) != 0) { - fprintf(stderr, "gsr warning: install_cuda_no_stable_perf_limit: failed to create directory: %s\n", nv_profiles_path); + gsr_log(GSR_LOG_LEVEL_WARNING, "install_cuda_no_stable_perf_limit: failed to create directory: %s", nv_profiles_path); return; } @@ -3622,7 +3613,7 @@ static void install_cuda_no_stable_perf_limit() { FILE *f = fopen(nv_profiles_path, "wb"); if(!f) { - fprintf(stderr, "gsr warning: install_cuda_no_stable_perf_limit: failed to create file: %s\n", nv_profiles_path); + gsr_log(GSR_LOG_LEVEL_WARNING, "install_cuda_no_stable_perf_limit: failed to create file: %s", nv_profiles_path); return; } @@ -3683,7 +3674,7 @@ int main(int argc, char **argv) { unsetenv("vblank_mode"); if(geteuid() == 0) { - fprintf(stderr, "gsr error: don't run gpu-screen-recorder as the root user\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "don't run gpu-screen-recorder as the root user"); _exit(1); } @@ -3711,7 +3702,7 @@ int main(int argc, char **argv) { std::vector capture_sources = parse_capture_source_arg(arg_parser.capture_source, arg_parser); if(capture_sources.empty()) { - fprintf(stderr, "gsr error: option -w can't be empty. You need to capture video from at least one source\n"); + 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(1); } @@ -3735,12 +3726,12 @@ int main(int argc, char **argv) { memset(&pipewire_audio, 0, sizeof(pipewire_audio)); if(uses_app_audio) { if(!pulseaudio_server_is_pipewire()) { - fprintf(stderr, "gsr error: your sound server is not PipeWire. Application audio is only available when running PipeWire audio server\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "your sound server is not PipeWire. Application audio is only available when running PipeWire audio server"); _exit(2); } if(!gsr_pipewire_audio_init(&pipewire_audio)) { - fprintf(stderr, "gsr error: failed to setup PipeWire audio for application audio capture\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "failed to setup PipeWire audio for application audio capture"); _exit(2); } @@ -3752,7 +3743,7 @@ int main(int argc, char **argv) { } #else if(uses_app_audio) { - fprintf(stderr, "gsr error: application audio can't be recorded because GPU Screen Recorder is built without application audio support (-Dapp_audio option)\n"); + 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(2); } #endif @@ -3765,7 +3756,7 @@ int main(int argc, char **argv) { XSelectInput(dpy, DefaultRootWindow(dpy), PropertyChangeMask); } else { wayland = true; - fprintf(stderr, "gsr warning: failed to connect to the X server. Assuming wayland is running without Xwayland\n"); + gsr_log(GSR_LOG_LEVEL_WARNING, "failed to connect to the X server. Assuming wayland is running without Xwayland"); } XSetErrorHandler(x11_error_handler); @@ -3778,31 +3769,31 @@ int main(int argc, char **argv) { // Disable prime-run and similar options as it doesn't work, the monitor to capture has to be run on the same device. // This is fine on wayland since nvidia uses drm interface there and the monitor query checks the monitors connected // to the drm device. - fprintf(stderr, "gsr warning: use of prime-run on X11 is not supported. Disabling prime-run\n"); + gsr_log(GSR_LOG_LEVEL_WARNING, "use of prime-run on X11 is not supported. Disabling prime-run"); disable_prime_run(); } gsr_window *window = gsr_window_create(dpy, wayland); if(!window) { - fprintf(stderr, "gsr error: failed to create window\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "failed to create window"); _exit(1); } if(is_capturing_type(capture_sources, GSR_CAPTURE_SOURCE_TYPE_PORTAL)) { if(is_using_prime_run()) { - fprintf(stderr, "gsr warning: use of prime-run with -w portal option is currently not supported. Disabling prime-run\n"); + gsr_log(GSR_LOG_LEVEL_WARNING, "use of prime-run with -w portal option is currently not supported. Disabling prime-run"); disable_prime_run(); } if(video_codec_is_hdr(arg_parser.video_codec)) { - fprintf(stderr, "gsr warning: portal capture option doesn't support hdr yet (PipeWire doesn't support hdr), the video will be tonemapped from hdr to sdr\n"); + 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.video_codec = hdr_video_codec_to_sdr_video_codec(arg_parser.video_codec); } } gsr_egl egl; if(!gsr_egl_load(&egl, window, is_capturing_monitor_or_region(capture_sources), arg_parser.gl_debug)) { - fprintf(stderr, "gsr error: failed to load opengl\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "failed to load opengl"); _exit(1); } @@ -3818,7 +3809,7 @@ int main(int argc, char **argv) { if(monitor_capture_use_drm(window, egl.gpu_info.vendor)) { // TODO: Allow specifying another card, and in other places if(!gsr_get_valid_card_path(&egl, egl.card_path, is_capturing_monitor_or_region(capture_sources))) { - fprintf(stderr, "gsr 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\n"); + 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(2); } } else { @@ -3840,7 +3831,7 @@ int main(int argc, char **argv) { gsr_image_format image_format; if(get_image_format_from_filename(arg_parser.filename, &image_format)) { if(audio_input_arg->num_values > 0) { - fprintf(stderr, "gsr error: can't record audio (-a) when taking a screenshot\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "can't record audio (-a) when taking a screenshot"); _exit(1); } @@ -3853,9 +3844,9 @@ int main(int argc, char **argv) { avformat_alloc_output_context2(&av_format_context, nullptr, arg_parser.container_format, arg_parser.filename); if (!av_format_context) { if(arg_parser.container_format) { - fprintf(stderr, "gsr error: Container format '%s' (argument -c) is not valid\n", arg_parser.container_format); + gsr_log(GSR_LOG_LEVEL_ERROR, "Container format '%s' (argument -c) is not valid", arg_parser.container_format); } else { - fprintf(stderr, "gsr error: Failed to deduce container format from file extension. Use the '-c' option to specify container format\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "Failed to deduce container format from file extension. Use the '-c' option to specify container format"); args_parser_print_usage(); _exit(1); } @@ -3888,7 +3879,7 @@ int main(int argc, char **argv) { // (Some?) livestreaming services require at least one audio track to work. // If not audio is provided then create one silent audio track. if(arg_parser.is_livestream && requested_audio_inputs.empty()) { - fprintf(stderr, "gsr info: live streaming but no audio track was added. Adding a silent audio track\n"); + gsr_log(GSR_LOG_LEVEL_INFO, "live streaming but no audio track was added. Adding a silent audio track"); MergedAudioInputs mai; mai.audio_inputs.push_back({""}); requested_audio_inputs.push_back(std::move(mai)); @@ -3898,7 +3889,7 @@ int main(int argc, char **argv) { std::vector audio_tracks; if(arg_parser.video_encoder == GSR_VIDEO_ENCODER_HW_CPU && arg_parser.video_codec != (gsr_video_codec)GSR_VIDEO_CODEC_AUTO && arg_parser.video_codec != GSR_VIDEO_CODEC_H264) { - fprintf(stderr, "gsr error: -encoder cpu was specified but a codec other than h264 was specified. -encoder cpu supports only h264 at the moment\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "-encoder cpu was specified but a codec other than h264 was specified. -encoder cpu supports only h264 at the moment"); _exit(1); } @@ -3912,7 +3903,7 @@ int main(int argc, char **argv) { AVFrame *video_frame = av_frame_alloc(); if(!video_frame) { - fprintf(stderr, "gsr error: Failed to allocate video frame\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "Failed to allocate video frame"); _exit(1); } video_frame->format = video_codec_context->pix_fmt; @@ -3927,18 +3918,18 @@ int main(int argc, char **argv) { const size_t estimated_replay_buffer_packets = calculate_estimated_replay_buffer_packets(arg_parser.replay_buffer_size_secs, arg_parser.fps, arg_parser.audio_codec, requested_audio_inputs); gsr_encoder encoder; if(!gsr_encoder_init(&encoder, arg_parser.replay_storage, estimated_replay_buffer_packets, arg_parser.replay_buffer_size_secs, arg_parser.filename)) { - fprintf(stderr, "gsr error: failed to create encoder\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "failed to create encoder"); _exit(1); } gsr_video_encoder *video_encoder = create_video_encoder(&egl, arg_parser); if(!video_encoder) { - fprintf(stderr, "gsr error: failed to create video encoder\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "failed to create video encoder"); _exit(1); } if(!gsr_video_encoder_start(video_encoder, video_codec_context, video_frame)) { - fprintf(stderr, "gsr error: failed to start video encoder\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "failed to start video encoder"); _exit(1); } @@ -3964,7 +3955,7 @@ int main(int argc, char **argv) { gsr_color_conversion color_conversion; if(gsr_color_conversion_init(&color_conversion, &color_conversion_params) != 0) { - fprintf(stderr, "gsr error: main: failed to create color conversion\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "main: failed to create color conversion"); _exit(1); } @@ -3997,7 +3988,7 @@ int main(int argc, char **argv) { if(!arg_parser.is_replaying) { audio_stream = create_stream(av_format_context, audio_codec_context); if(gsr_encoder_add_recording_destination(&encoder, audio_codec_context, av_format_context, audio_stream, 0) == (size_t)-1) - fprintf(stderr, "gsr error: added too many audio sources\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "added too many audio sources"); } if(audio_stream && !merged_audio_inputs.track_name.empty() && !arg_parser.exclude_metadata) @@ -4021,7 +4012,7 @@ int main(int argc, char **argv) { if(use_amix) { int err = init_filter_graph(audio_codec_context, &graph, &sink, src_filter_ctx, merged_audio_inputs.audio_inputs.size()); if(err < 0) { - fprintf(stderr, "gsr error: failed to create audio filter\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "failed to create audio filter"); _exit(1); } } @@ -4063,7 +4054,7 @@ int main(int argc, char **argv) { if(!arg_parser.is_replaying && !(output_format->flags & AVFMT_NOFILE)) { const int ret = avio_open(&av_format_context->pb, arg_parser.filename, AVIO_FLAG_WRITE); if(ret < 0) { - fprintf(stderr, "gsr error: Could not open '%s': %s\n", arg_parser.filename, av_error_to_string(ret)); + gsr_log(GSR_LOG_LEVEL_ERROR, "Could not open '%s': %s", arg_parser.filename, av_error_to_string(ret)); _exit(1); } } @@ -4092,7 +4083,7 @@ int main(int argc, char **argv) { const size_t audio_buffer_size = audio_max_frame_size * 4 * 2; // max 4 bytes/sample, 2 channels uint8_t *empty_audio = (uint8_t*)malloc(audio_buffer_size); if(!empty_audio) { - fprintf(stderr, "gsr error: failed to create empty audio\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "failed to create empty audio"); _exit(1); } memset(empty_audio, 0, audio_buffer_size); @@ -4198,7 +4189,7 @@ int main(int argc, char **argv) { if(audio_track.graph) { // TODO: av_buffersrc_add_frame if(av_buffersrc_write_frame(audio_device.src_filter_ctx, audio_device.frame) < 0) { - fprintf(stderr, "gsr error: failed to add audio frame to filter\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "failed to add audio frame to filter"); } } else { ret = avcodec_send_frame(audio_track.codec_context, audio_device.frame); @@ -4239,7 +4230,7 @@ int main(int argc, char **argv) { if(audio_track.graph) { // TODO: av_buffersrc_add_frame if(av_buffersrc_write_frame(audio_device.src_filter_ctx, audio_device.frame) < 0) { - fprintf(stderr, "gsr error: failed to add audio frame to filter\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "failed to add audio frame to filter"); } } else { ret = avcodec_send_frame(audio_track.codec_context, audio_device.frame); @@ -4335,7 +4326,7 @@ int main(int argc, char **argv) { } } } else if(is_capturing_monitor_or_region(capture_sources)) { - fprintf(stderr, "gsr warning: \"-fm content\" has no effect on Wayland when recording a monitor. Either record a monitor on X11 or capture with desktop portal instead (-w portal)\n"); + gsr_log(GSR_LOG_LEVEL_WARNING, "\"-fm content\" has no effect on Wayland when recording a monitor. Either record a monitor on X11 or capture with desktop portal instead (-w portal)"); } } @@ -4424,7 +4415,7 @@ int main(int argc, char **argv) { if(kms_client_initialized) { if(gsr_kms_client_get_kms(&kms_client, &kms_response) != 0) - fprintf(stderr, "gsr error: failed to get kms, error: %d (%s)\n", kms_response.result, kms_response.err_msg); + gsr_log(GSR_LOG_LEVEL_ERROR, "failed to get kms, error: %d (%s)", kms_response.result, kms_response.err_msg); } bool capture_has_synchronous_task = false; @@ -4500,7 +4491,7 @@ int main(int argc, char **argv) { // TODO: Move to separate thread because this could write to network (for example when livestreaming) gsr_encoder_receive_packets(&encoder, video_codec_context, video_frame->pts, VIDEO_STREAM_INDEX); } else { - fprintf(stderr, "gsr error: avcodec_send_frame failed, error: %s\n", av_error_to_string(ret)); + gsr_log(GSR_LOG_LEVEL_ERROR, "avcodec_send_frame failed, error: %s", av_error_to_string(ret)); } if(force_iframe_frame) { diff --git a/src/pipewire_audio.c b/src/pipewire_audio.c index ec4fde1..be1ba21 100644 --- a/src/pipewire_audio.c +++ b/src/pipewire_audio.c @@ -1,4 +1,5 @@ #include "../include/pipewire_audio.h" +#include "../include/log.h" #include #include @@ -388,7 +389,7 @@ static bool gsr_pipewire_audio_listen_on_metadata(gsr_pipewire_audio *self, uint self->metadata_proxy = pw_registry_bind(self->registry, id, PW_TYPE_INTERFACE_Metadata, PW_VERSION_METADATA, 0); if(!self->metadata_proxy) { - fprintf(stderr, "gsr error: gsr_pipewire_audio_listen_on_metadata: failed to bind to registry\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_pipewire_audio_listen_on_metadata: failed to bind to registry"); return false; } @@ -407,7 +408,7 @@ static bool array_ensure_capacity(void **array, size_t size, size_t *capacity_it void *new_data = realloc(*array, new_capacity_items * element_size); if(!new_data) { - fprintf(stderr, "gsr error: pipewire_audio: failed to reallocate memory\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "pipewire_audio: failed to reallocate memory"); return false; } @@ -533,7 +534,7 @@ static void gsr_pipewire_audio_bind_node(gsr_pipewire_audio *self, uint32_t id) struct pw_proxy *proxy = pw_registry_bind(self->registry, id, PW_TYPE_INTERFACE_Node, PW_VERSION_NODE, 0); if(!proxy) { - fprintf(stderr, "gsr error: gsr_pipewire_audio_bind_node: failed to bind to node %u\n", id); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_pipewire_audio_bind_node: failed to bind to node %u", id); free(node_binding); return; } @@ -701,14 +702,14 @@ bool gsr_pipewire_audio_init(gsr_pipewire_audio *self) { self->thread_loop = pw_thread_loop_new("gsr screen capture", NULL); if(!self->thread_loop) { - fprintf(stderr, "gsr error: gsr_pipewire_audio_init: failed to create pipewire thread\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_pipewire_audio_init: failed to create pipewire thread"); gsr_pipewire_audio_deinit(self); return false; } self->context = pw_context_new(pw_thread_loop_get_loop(self->thread_loop), NULL, 0); if(!self->context) { - fprintf(stderr, "gsr error: gsr_pipewire_audio_init: failed to create pipewire context\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_pipewire_audio_init: failed to create pipewire context"); gsr_pipewire_audio_deinit(self); return false; } @@ -716,7 +717,7 @@ bool gsr_pipewire_audio_init(gsr_pipewire_audio *self) { pw_context_load_module(self->context, "libpipewire-module-link-factory", NULL, NULL); if(pw_thread_loop_start(self->thread_loop) < 0) { - fprintf(stderr, "gsr error: gsr_pipewire_audio_init: failed to start thread\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_pipewire_audio_init: failed to start thread"); gsr_pipewire_audio_deinit(self); return false; } diff --git a/src/pipewire_video.c b/src/pipewire_video.c index 18f6c9a..80cef7e 100644 --- a/src/pipewire_video.c +++ b/src/pipewire_video.c @@ -1,4 +1,5 @@ #include "../include/pipewire_video.h" +#include "../include/log.h" #include "../include/egl.h" #include "../include/utils.h" @@ -62,11 +63,11 @@ static bool check_pw_version(const gsr_pipewire_video_data_version *pw_version, } static void update_pw_versions(gsr_pipewire_video *self, const char *version) { - fprintf(stderr, "gsr info: pipewire: server version: %s\n", version); - fprintf(stderr, "gsr info: pipewire: library version: %s\n", pw_get_library_version()); - fprintf(stderr, "gsr info: pipewire: header version: %s\n", pw_get_headers_version()); + gsr_log(GSR_LOG_LEVEL_INFO, "pipewire: server version: %s", version); + gsr_log(GSR_LOG_LEVEL_INFO, "pipewire: library version: %s", pw_get_library_version()); + gsr_log(GSR_LOG_LEVEL_INFO, "pipewire: header version: %s", pw_get_headers_version()); if(!parse_pw_version(&self->server_version, version)) - fprintf(stderr, "gsr error: pipewire: failed to parse server version\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "pipewire: failed to parse server version"); } static void on_core_info_cb(void *user_data, const struct pw_core_info *info) { @@ -76,7 +77,7 @@ static void on_core_info_cb(void *user_data, const struct pw_core_info *info) { static void on_core_error_cb(void *user_data, uint32_t id, int seq, int res, const char *message) { gsr_pipewire_video *self = user_data; - fprintf(stderr, "gsr error: pipewire: error id:%u seq:%d res:%d: %s\n", id, seq, res, message); + gsr_log(GSR_LOG_LEVEL_ERROR, "pipewire: error id:%u seq:%d res:%d: %s", id, seq, res, message); pw_thread_loop_signal(self->thread_loop, false); } @@ -138,8 +139,7 @@ static void gsr_pipewire_video_read_cursor_metadata(gsr_pipewire_video *self, st if(bitmap && bitmap->size.width > 0 && bitmap->size.height > 0 && is_cursor_format_supported(bitmap->format)) { /* Animated cursors update the bitmap for every animation frame, only log when the size changes */ if((int)bitmap->size.width != self->cursor.width || (int)bitmap->size.height != self->cursor.height) { - fprintf(stderr, "gsr info: pipewire: cursor bitmap update, size: %dx%d, format: %s\n", - (int)bitmap->size.width, (int)bitmap->size.height, spa_debug_type_find_name(spa_type_video_format, bitmap->format)); + gsr_log(GSR_LOG_LEVEL_INFO, "pipewire: cursor bitmap update, size: %dx%d, format: %s", (int)bitmap->size.width, (int)bitmap->size.height, spa_debug_type_find_name(spa_type_video_format, bitmap->format)); } const uint8_t *bitmap_data = SPA_MEMBER(bitmap, bitmap->offset, uint8_t); @@ -205,7 +205,7 @@ static void on_process_cb(void *user_data) { } if(!got_buffer) { - fprintf(stderr, "gsr info: pipewire: out of buffers!\n"); + gsr_log(GSR_LOG_LEVEL_INFO, "pipewire: out of buffers!"); return; } @@ -315,19 +315,18 @@ static void on_param_changed_cb(void *user_data, uint32_t id, const struct spa_p if(has_modifier || check_pw_version(&self->server_version, 0, 3, 24)) buffer_types |= 1 << SPA_DATA_DmaBuf; - fprintf(stderr, "gsr info: pipewire: negotiated format:\n"); + gsr_log(GSR_LOG_LEVEL_INFO, "pipewire: negotiated format:"); - fprintf(stderr, "gsr info: pipewire: Format: %d (%s)\n", - self->format.info.raw.format, + gsr_log(GSR_LOG_LEVEL_INFO, "pipewire: Format: %d (%s)", self->format.info.raw.format, spa_debug_type_find_name(spa_type_video_format, self->format.info.raw.format)); self->has_modifier = has_modifier; if(self->has_modifier) { - fprintf(stderr, "gsr info: pipewire: Modifier: 0x%" PRIx64 "\n", self->format.info.raw.modifier); + gsr_log(GSR_LOG_LEVEL_INFO, "pipewire: Modifier: 0x%" PRIx64, self->format.info.raw.modifier); } - fprintf(stderr, "gsr info: pipewire: Size: %dx%d\n", self->format.info.raw.size.width, self->format.info.raw.size.height); - fprintf(stderr, "gsr info: pipewire: Framerate: %d/%d\n", self->format.info.raw.framerate.num, self->format.info.raw.framerate.denom); + gsr_log(GSR_LOG_LEVEL_INFO, "pipewire: Size: %dx%d", self->format.info.raw.size.width, self->format.info.raw.size.height); + gsr_log(GSR_LOG_LEVEL_INFO, "pipewire: Framerate: %d/%d", self->format.info.raw.framerate.num, self->format.info.raw.framerate.denom); uint8_t params_buffer[2048]; struct spa_pod_builder pod_builder = SPA_POD_BUILDER_INIT(params_buffer, sizeof(params_buffer)); @@ -378,14 +377,11 @@ static void on_param_changed_cb(void *user_data, uint32_t id, const struct spa_p static void on_state_changed_cb(void *user_data, enum pw_stream_state prev_state, enum pw_stream_state new_state, const char *error) { gsr_pipewire_video *self = user_data; - fprintf(stderr, "gsr info: pipewire: stream %p previous state: \"%s\", new state: \"%s\" (error: %s)\n", - (void*)self->stream, pw_stream_state_as_string(prev_state), pw_stream_state_as_string(new_state), + gsr_log(GSR_LOG_LEVEL_INFO, "pipewire: stream %p previous state: \"%s\", new state: \"%s\" (error: %s)", (void*)self->stream, pw_stream_state_as_string(prev_state), pw_stream_state_as_string(new_state), error ? error : "none"); if(new_state == PW_STREAM_STATE_ERROR && error && strstr(error, "alloc buffers")) { - fprintf(stderr, "gsr error: pipewire: the desktop portal failed to provide dmabuf (GPU) video frames. This happens when the " - "compositor only provides the frames as shared memory (CPU), which gpu-screen-recorder intentionally doesn't support for portal capture. " - "Capture the monitor directly (for example '-w screen' or '-w ', see 'gpu-screen-recorder --list-capture-options') instead of '-w portal'.\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "pipewire: the desktop portal failed to provide dmabuf (GPU) video frames. This happens when the compositor only provides the frames as shared memory (CPU), which gpu-screen-recorder intentionally doesn't support for portal capture. Capture the monitor directly (for example '-w screen' or '-w ', see 'gpu-screen-recorder --list-capture-options') instead of '-w portal'."); } pthread_mutex_lock(&self->mutex); @@ -528,7 +524,7 @@ static void renegotiate_format(void *data, uint64_t expirations) { uint8_t params_buffer[8192]; struct spa_pod_builder pod_builder = SPA_POD_BUILDER_INIT(params_buffer, sizeof(params_buffer)); if (!gsr_pipewire_video_build_format_params(self, &pod_builder, params, &num_video_formats)) { - fprintf(stderr, "gsr error: renegotiate_format: failed to build formats\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "renegotiate_format: failed to build formats"); pw_thread_loop_unlock(self->thread_loop); return; } @@ -541,7 +537,7 @@ static bool spa_video_format_get_modifiers(gsr_pipewire_video *self, const enum *num_modifiers = 0; if(max_modifiers == 0) { - fprintf(stderr, "gsr error: spa_video_format_get_modifiers: no space for modifiers left\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "spa_video_format_get_modifiers: no space for modifiers left"); //modifiers[0] = DRM_FORMAT_MOD_LINEAR; //modifiers[1] = DRM_FORMAT_MOD_INVALID; //*num_modifiers = 2; @@ -549,7 +545,7 @@ static bool spa_video_format_get_modifiers(gsr_pipewire_video *self, const enum } if(!self->egl->eglQueryDmaBufModifiersEXT) { - fprintf(stderr, "gsr error: spa_video_format_get_modifiers: failed to initialize modifiers because eglQueryDmaBufModifiersEXT is not available\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "spa_video_format_get_modifiers: failed to initialize modifiers because eglQueryDmaBufModifiersEXT is not available"); //modifiers[0] = DRM_FORMAT_MOD_LINEAR; //modifiers[1] = DRM_FORMAT_MOD_INVALID; //*num_modifiers = 2; @@ -560,12 +556,12 @@ static bool spa_video_format_get_modifiers(gsr_pipewire_video *self, const enum const int64_t drm_format = spa_video_format_to_drm_format(format); if(drm_format == DRM_FORMAT_INVALID) { - fprintf(stderr, "gsr error: spa_video_format_get_modifiers: unsupported format: %d\n", (int)format); + gsr_log(GSR_LOG_LEVEL_ERROR, "spa_video_format_get_modifiers: unsupported format: %d", (int)format); return false; } if(!self->egl->eglQueryDmaBufModifiersEXT(self->egl->egl_display, drm_format, max_modifiers, modifiers, NULL, num_modifiers)) { - fprintf(stderr, "gsr error: spa_video_format_get_modifiers: eglQueryDmaBufModifiersEXT failed with drm format %d, %" PRIi64 "\n", (int)format, drm_format); + gsr_log(GSR_LOG_LEVEL_ERROR, "spa_video_format_get_modifiers: eglQueryDmaBufModifiersEXT failed with drm format %d, %" PRIi64, (int)format, drm_format); modifiers[0] = DRM_FORMAT_MOD_INVALID; *num_modifiers = 1; return false; @@ -618,18 +614,18 @@ static bool gsr_pipewire_video_setup_stream(gsr_pipewire_video *self) { self->thread_loop = pw_thread_loop_new("gsr screen capture", NULL); if(!self->thread_loop) { - fprintf(stderr, "gsr error: gsr_pipewire_video_setup_stream: failed to create pipewire thread\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_pipewire_video_setup_stream: failed to create pipewire thread"); goto error; } self->context = pw_context_new(pw_thread_loop_get_loop(self->thread_loop), NULL, 0); if(!self->context) { - fprintf(stderr, "gsr error: gsr_pipewire_video_setup_stream: failed to create pipewire context\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_pipewire_video_setup_stream: failed to create pipewire context"); goto error; } if(pw_thread_loop_start(self->thread_loop) < 0) { - fprintf(stderr, "gsr error: gsr_pipewire_video_setup_stream: failed to start thread\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_pipewire_video_setup_stream: failed to start thread"); goto error; } @@ -639,7 +635,7 @@ static bool gsr_pipewire_video_setup_stream(gsr_pipewire_video *self) { self->core = pw_context_connect_fd(self->context, fcntl(self->fd, F_DUPFD_CLOEXEC, 5), NULL, 0); if(!self->core) { pw_thread_loop_unlock(self->thread_loop); - fprintf(stderr, "gsr error: gsr_pipewire_video_setup_stream: failed to connect to fd %d\n", self->fd); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_pipewire_video_setup_stream: failed to connect to fd %d", self->fd); goto error; } @@ -655,7 +651,7 @@ static bool gsr_pipewire_video_setup_stream(gsr_pipewire_video *self) { self->reneg = pw_loop_add_event(pw_thread_loop_get_loop(self->thread_loop), renegotiate_format, self); if(!self->reneg) { pw_thread_loop_unlock(self->thread_loop); - fprintf(stderr, "gsr error: gsr_pipewire_video_setup_stream: pw_loop_add_event failed\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_pipewire_video_setup_stream: pw_loop_add_event failed"); goto error; } @@ -665,14 +661,14 @@ static bool gsr_pipewire_video_setup_stream(gsr_pipewire_video *self) { PW_KEY_MEDIA_ROLE, "Screen", NULL)); if(!self->stream) { pw_thread_loop_unlock(self->thread_loop); - fprintf(stderr, "gsr error: gsr_pipewire_video_setup_stream: failed to create stream\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_pipewire_video_setup_stream: failed to create stream"); goto error; } pw_stream_add_listener(self->stream, &self->stream_listener, &stream_events, self); if(!gsr_pipewire_video_build_format_params(self, &pod_builder, params, &num_video_formats)) { pw_thread_loop_unlock(self->thread_loop); - fprintf(stderr, "gsr error: gsr_pipewire_video_setup_stream: failed to build format params\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_pipewire_video_setup_stream: failed to build format params"); goto error; } @@ -682,7 +678,7 @@ static bool gsr_pipewire_video_setup_stream(gsr_pipewire_video *self) { num_video_formats) < 0) { pw_thread_loop_unlock(self->thread_loop); - fprintf(stderr, "gsr error: gsr_pipewire_video_setup_stream: failed to connect stream\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_pipewire_video_setup_stream: failed to connect stream"); goto error; } @@ -729,7 +725,7 @@ bool gsr_pipewire_video_init(gsr_pipewire_video *self, int pipewire_fd, uint32_t self->fd = pipewire_fd; self->node = pipewire_node; if(pthread_mutex_init(&self->mutex, NULL) != 0) { - fprintf(stderr, "gsr error: gsr_pipewire_video_init: failed to initialize mutex\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_pipewire_video_init: failed to initialize mutex"); gsr_pipewire_video_deinit(self); return false; } @@ -839,11 +835,11 @@ static EGLImage gsr_pipewire_video_create_egl_image_with_fallback(gsr_pipewire_v image = gsr_pipewire_video_create_egl_image(self, fds, offsets, pitches, modifiers, true); if(!image) { if(self->format.info.raw.modifier == DRM_FORMAT_MOD_INVALID) { - fprintf(stderr, "gsr error: gsr_pipewire_video_create_egl_image_with_fallback: failed to create egl image with modifiers, trying without modifiers\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_pipewire_video_create_egl_image_with_fallback: failed to create egl image with modifiers, trying without modifiers"); self->no_modifiers_fallback = true; image = gsr_pipewire_video_create_egl_image(self, fds, offsets, pitches, modifiers, false); } else { - fprintf(stderr, "gsr error: gsr_pipewire_video_create_egl_image_with_fallback: failed to create egl image with modifier 0x%" PRIx64 ", renegotiating with a different modifier\n", self->format.info.raw.modifier); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_pipewire_video_create_egl_image_with_fallback: failed to create egl image with modifier 0x%" PRIx64 ", renegotiating with a different modifier", self->format.info.raw.modifier); self->negotiated = false; pw_thread_loop_lock(self->thread_loop); gsr_pipewire_video_remove_modifier(self, self->format.info.raw.modifier); @@ -870,7 +866,7 @@ static void gsr_pipewire_video_bind_image_to_texture_with_fallback(gsr_pipewire_ gsr_pipewire_video_bind_image_to_texture(self, image, texture_map.external_texture_id, true); } else { if(!gsr_pipewire_video_bind_image_to_texture(self, image, texture_map.texture_id, false)) { - fprintf(stderr, "gsr error: gsr_pipewire_video_map_texture: failed to bind image to texture, trying with external texture\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_pipewire_video_map_texture: failed to bind image to texture, trying with external texture"); self->external_texture_fallback = true; gsr_pipewire_video_bind_image_to_texture(self, image, texture_map.external_texture_id, true); } diff --git a/src/plugins.c b/src/plugins.c index 053b2a3..7686683 100644 --- a/src/plugins.c +++ b/src/plugins.c @@ -1,4 +1,5 @@ #include "../include/plugins.h" +#include "../include/log.h" #include "../include/utils.h" #include #include @@ -23,7 +24,7 @@ bool gsr_plugins_init(gsr_plugins *self, gsr_plugin_init_params init_params, gsr const unsigned int texture = gl_create_texture(egl, init_params.width, init_params.height, color_depth_to_gl_internal_format(init_params.color_depth), GL_RGBA, GL_LINEAR); if(texture == 0) { - fprintf(stderr, "gsr error: gsr_plugins_init failed to create texture\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_plugins_init failed to create texture"); return false; } self->texture = texture; @@ -41,7 +42,7 @@ bool gsr_plugins_init(gsr_plugins *self, gsr_plugin_init_params init_params, gsr color_conversion_params.destination_textures[0] = self->texture; if(gsr_color_conversion_init(&self->color_conversion, &color_conversion_params) != 0) { - fprintf(stderr, "gsr error: gsr_plugins_init failed to create color conversion\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_plugins_init failed to create color conversion"); gsr_plugins_deinit(self); return false; } @@ -54,7 +55,7 @@ void gsr_plugins_deinit(gsr_plugins *self) { for(int i = self->num_plugins - 1; i >= 0; --i) { gsr_plugin *plugin = &self->plugins[i]; plugin->gsr_plugin_deinit(plugin->data.userdata); - fprintf(stderr, "gsr info: unloaded plugin: %s\n", plugin->data.name); + gsr_log(GSR_LOG_LEVEL_INFO, "unloaded plugin: %s", plugin->data.name); } self->num_plugins = 0; @@ -68,7 +69,7 @@ void gsr_plugins_deinit(gsr_plugins *self) { bool gsr_plugins_load_plugin(gsr_plugins *self, const char *plugin_filepath) { if(self->num_plugins >= GSR_MAX_PLUGINS) { - fprintf(stderr, "gsr error: gsr_plugins_load_plugin failed, more plugins can't load more than %d plugins. Report this as an issue\n", GSR_MAX_PLUGINS); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_plugins_load_plugin failed, more plugins can't load more than %d plugins. Report this as an issue", GSR_MAX_PLUGINS); return false; } @@ -77,38 +78,38 @@ bool gsr_plugins_load_plugin(gsr_plugins *self, const char *plugin_filepath) { plugin.lib = dlopen(plugin_filepath, RTLD_LAZY); if(!plugin.lib) { - fprintf(stderr, "gsr error: gsr_plugins_load_plugin failed to load \"%s\", error: %s\n", plugin_filepath, dlerror()); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_plugins_load_plugin failed to load \"%s\", error: %s", plugin_filepath, dlerror()); return false; } plugin.gsr_plugin_init = dlsym(plugin.lib, "gsr_plugin_init"); if(!plugin.gsr_plugin_init) { - fprintf(stderr, "gsr error: gsr_plugins_load_plugin failed to find \"gsr_plugin_init\" in plugin \"%s\"\n", plugin_filepath); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_plugins_load_plugin failed to find \"gsr_plugin_init\" in plugin \"%s\"", plugin_filepath); goto fail; } plugin.gsr_plugin_deinit = dlsym(plugin.lib, "gsr_plugin_deinit"); if(!plugin.gsr_plugin_deinit) { - fprintf(stderr, "gsr error: gsr_plugins_load_plugin failed to find \"gsr_plugin_deinit\" in plugin \"%s\"\n", plugin_filepath); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_plugins_load_plugin failed to find \"gsr_plugin_deinit\" in plugin \"%s\"", plugin_filepath); goto fail; } if(!plugin.gsr_plugin_init(&self->init_params, &plugin.data)) { - fprintf(stderr, "gsr error: gsr_plugins_load_plugin failed to load plugin \"%s\", gsr_plugin_init in the plugin failed\n", plugin_filepath); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_plugins_load_plugin failed to load plugin \"%s\", gsr_plugin_init in the plugin failed", plugin_filepath); goto fail; } if(!plugin.data.name) { - fprintf(stderr, "gsr error: gsr_plugins_load_plugin failed to load plugin \"%s\", the plugin didn't set the name (gsr_plugin_init_return.name)\n", plugin_filepath); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_plugins_load_plugin failed to load plugin \"%s\", the plugin didn't set the name (gsr_plugin_init_return.name)", plugin_filepath); goto fail; } if(plugin.data.version == 0) { - fprintf(stderr, "gsr error: gsr_plugins_load_plugin failed to load plugin \"%s\", the plugin didn't set the version (gsr_plugin_init_return.version)\n", plugin_filepath); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_plugins_load_plugin failed to load plugin \"%s\", the plugin didn't set the version (gsr_plugin_init_return.version)", plugin_filepath); goto fail; } - fprintf(stderr, "gsr info: loaded plugin: %s, name: %s, version: %u\n", plugin_filepath, plugin.data.name, plugin.data.version); + gsr_log(GSR_LOG_LEVEL_INFO, "loaded plugin: %s, name: %s, version: %u", plugin_filepath, plugin.data.name, plugin.data.version); self->plugins[self->num_plugins] = plugin; ++self->num_plugins; return true; diff --git a/src/replay_buffer/replay_buffer_disk.c b/src/replay_buffer/replay_buffer_disk.c index ce42d93..9a0d247 100644 --- a/src/replay_buffer/replay_buffer_disk.c +++ b/src/replay_buffer/replay_buffer_disk.c @@ -1,4 +1,5 @@ #include "../../include/replay_buffer/replay_buffer_disk.h" +#include "../../include/log.h" #include "../../include/utils.h" #include @@ -25,12 +26,12 @@ static void gsr_av_packet_disk_init(gsr_av_packet_disk *self, const AVPacket *av static gsr_replay_buffer_file* gsr_replay_buffer_file_create(char *replay_directory, size_t replay_storage_counter, double timestamp, int *replay_storage_fd) { gsr_replay_buffer_file *self = calloc(1, sizeof(gsr_replay_buffer_file)); if(!self) { - fprintf(stderr, "gsr error: gsr_av_packet_file_init: failed to create buffer file\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_av_packet_file_init: failed to create buffer file"); return NULL; } if(create_directory_recursive(replay_directory) != 0) { - fprintf(stderr, "gsr error: gsr_av_packet_file_init: failed to create replay directory: %s\n", replay_directory); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_av_packet_file_init: failed to create replay directory: %s", replay_directory); free(self); return NULL; } @@ -39,7 +40,7 @@ static gsr_replay_buffer_file* gsr_replay_buffer_file_create(char *replay_direct snprintf(filename, sizeof(filename), "%s/%s_%d.gsr", replay_directory, FILE_PREFIX, (int)replay_storage_counter); *replay_storage_fd = creat(filename, 0700); if(*replay_storage_fd <= 0) { - fprintf(stderr, "gsr error: gsr_av_packet_file_init: failed to create replay file: %s\n", filename); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_av_packet_file_init: failed to create replay file: %s", filename); free(self); return NULL; } @@ -135,7 +136,7 @@ static bool file_write_all(int fd, const uint8_t *data, size_t size, size_t *byt static bool gsr_replay_buffer_disk_create_next_file(gsr_replay_buffer_disk *self, double timestamp) { if(self->num_files + 1 >= GSR_REPLAY_BUFFER_CAPACITY_NUM_FILES) { - fprintf(stderr, "gsr error: gsr_replay_buffer_disk_create_next_file: too many replay buffer files created! (> %d), either reduce the replay buffer time or report this as a bug\n", (int)GSR_REPLAY_BUFFER_CAPACITY_NUM_FILES); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_replay_buffer_disk_create_next_file: too many replay buffer files created! (> %d), either reduce the replay buffer time or report this as a bug", (int)GSR_REPLAY_BUFFER_CAPACITY_NUM_FILES); return false; } @@ -160,7 +161,7 @@ static bool gsr_replay_buffer_disk_append_to_current_file(gsr_replay_buffer_disk void *new_packets = realloc(replay_buffer_file->packets, new_capacity_num_packets * sizeof(gsr_av_packet_disk)); if(!new_packets) { - fprintf(stderr, "gsr error: gsr_replay_buffer_disk_append_to_current_file: failed to reallocate replay buffer file packets\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_replay_buffer_disk_append_to_current_file: failed to reallocate replay buffer file packets"); return false; } @@ -233,20 +234,20 @@ static uint8_t* gsr_replay_buffer_disk_iterator_get_packet_data(gsr_replay_buffe snprintf(filename, sizeof(filename), "%s/%s_%d.gsr", self->replay_directory, FILE_PREFIX, (int)file->id); file->fd = open(filename, O_RDONLY); if(file->fd <= 0) { - fprintf(stderr, "gsr error: gsr_replay_buffer_disk_iterator_get_packet_data: failed to open file\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_replay_buffer_disk_iterator_get_packet_data: failed to open file"); return NULL; } } const gsr_av_packet_disk *packet = &self->files[iterator.file_index]->packets[iterator.packet_index]; if(lseek(file->fd, packet->data_index, SEEK_SET) == -1) { - fprintf(stderr, "gsr error: gsr_replay_buffer_disk_iterator_get_packet_data: failed to seek\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_replay_buffer_disk_iterator_get_packet_data: failed to seek"); return NULL; } uint8_t *packet_data = malloc(packet->packet.size); if(read(file->fd, packet_data, packet->packet.size) != packet->packet.size) { - fprintf(stderr, "gsr error: gsr_replay_buffer_disk_iterator_get_packet_data: failed to read data from file\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_replay_buffer_disk_iterator_get_packet_data: failed to read data from file"); free(packet_data); return NULL; } diff --git a/src/shader.c b/src/shader.c index f20ebb2..17315dc 100644 --- a/src/shader.c +++ b/src/shader.c @@ -1,4 +1,5 @@ #include "../include/shader.h" +#include "../include/log.h" #include "../include/egl.h" #include #include @@ -12,7 +13,7 @@ static int min_int(int a, int b) { static unsigned int load_shader(gsr_egl *egl, unsigned int type, const char *source) { unsigned int shader_id = egl->glCreateShader(type); if(shader_id == 0) { - fprintf(stderr, "gsr error: load_shader: failed to create shader, error: %d\n", egl->glGetError()); + gsr_log(GSR_LOG_LEVEL_ERROR, "load_shader: failed to create shader, error: %d", egl->glGetError()); return 0; } @@ -28,7 +29,7 @@ static unsigned int load_shader(gsr_egl *egl, unsigned int type, const char *sou if(info_length > 1 && print_compile_errors) { char info_log[4096]; egl->glGetShaderInfoLog(shader_id, min_int(4096, info_length), NULL, info_log); - fprintf(stderr, "gsr error: load_shader: failed to compile shader, error:\n%s\nshader source:\n%s\n", info_log, source); + gsr_log(GSR_LOG_LEVEL_ERROR, "load_shader: failed to compile shader, error:\n%s\nshader source:\n%s", info_log, source); } egl->glDeleteShader(shader_id); @@ -59,7 +60,7 @@ static unsigned int load_program(gsr_egl *egl, const char *vertex_shader, const program_id = egl->glCreateProgram(); if(program_id == 0) { - fprintf(stderr, "gsr error: load_program: failed to create shader program, error: %d\n", egl->glGetError()); + gsr_log(GSR_LOG_LEVEL_ERROR, "load_program: failed to create shader program, error: %d", egl->glGetError()); goto done; } @@ -79,7 +80,7 @@ static unsigned int load_program(gsr_egl *egl, const char *vertex_shader, const if(info_length > 1) { char info_log[4096]; egl->glGetProgramInfoLog(program_id, min_int(4096, info_length), NULL, info_log); - fprintf(stderr, "gsr error: load program: linking shader program failed, error:\n%s\n", info_log); + gsr_log(GSR_LOG_LEVEL_ERROR, "load program: linking shader program failed, error:\n%s", info_log); } goto done; @@ -106,7 +107,7 @@ int gsr_shader_init(gsr_shader *self, gsr_egl *egl, const char *vertex_shader, c self->program_id = 0; if(!vertex_shader && !fragment_shader) { - fprintf(stderr, "gsr error: gsr_shader_init: vertex and fragment shader can't be NULL at the same time\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_shader_init: vertex and fragment shader can't be NULL at the same time"); return -1; } diff --git a/src/sound.cpp b/src/sound.cpp index 4e04d8f..9a72e78 100644 --- a/src/sound.cpp +++ b/src/sound.cpp @@ -1,4 +1,5 @@ #include "../include/sound.hpp" +#include "../include/log.h" extern "C" { #include "../include/utils.h" } @@ -163,7 +164,7 @@ static bool startup_get_default_devices(pa_handle *p, const char *device_name) { } if(p->default_output_device_name[0] == '\0') { - fprintf(stderr, "gsr error: failed to find default audio output device\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "failed to find default audio output device"); return false; } @@ -207,7 +208,7 @@ static pa_handle* pa_sound_device_new(const char *server, const int buffer_size = attr->fragsize; void *buffer = malloc(buffer_size); if(!buffer) { - fprintf(stderr, "gsr error: failed to allocate buffer for audio\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "failed to allocate buffer for audio"); *rerror = -1; return NULL; } @@ -286,12 +287,12 @@ static bool pa_sound_device_handle_context_recreate(pa_handle *p) { } if (!(p->context = pa_context_new_with_proplist(pa_mainloop_get_api(p->mainloop), p->node_name, p->proplist))) { - fprintf(stderr, "gsr error: pa_context_new_with_proplist failed\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "pa_context_new_with_proplist failed"); goto fail; } if(pa_context_connect(p->context, nullptr, PA_CONTEXT_NOFLAGS, NULL) < 0) { - fprintf(stderr, "gsr error: pa_context_connect failed\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "pa_context_connect failed"); goto fail; } @@ -505,7 +506,7 @@ int sound_device_get_by_name(SoundDevice *device, const char *node_name, const c int error = 0; pa_handle *handle = pa_sound_device_new(nullptr, node_name, device_name, description, &ss, &buffer_attr, &error); if(!handle) { - fprintf(stderr, "gsr error: pa_sound_device_new() failed: %s. Audio input device %s might not be valid\n", pa_strerror(error), device_name); + gsr_log(GSR_LOG_LEVEL_ERROR, "pa_sound_device_new() failed: %s. Audio input device %s might not be valid", pa_strerror(error), device_name); return -1; } diff --git a/src/utils.c b/src/utils.c index 31853e2..75a6427 100644 --- a/src/utils.c +++ b/src/utils.c @@ -1,4 +1,5 @@ #include "../include/utils.h" +#include "../include/log.h" #include "../include/window/window.h" #include "../include/capture/capture.h" @@ -33,7 +34,7 @@ double clock_get_monotonic_seconds(void) { bool generate_random_characters(char *buffer, int buffer_size, const char *alphabet, size_t alphabet_size) { /* TODO: Use other functions on other platforms than linux */ if(getrandom(buffer, buffer_size, 0) < buffer_size) { - fprintf(stderr, "Failed to get random bytes, error: %s\n", strerror(errno)); + gsr_log(GSR_LOG_LEVEL_ERROR, "failed to get random bytes, error: %s", strerror(errno)); return false; } @@ -197,7 +198,7 @@ static bool connector_get_property_by_name(int drmfd, drmModeConnectorPtr props, static void for_each_active_monitor_output_drm(const char *card_path, active_monitor_callback callback, void *userdata) { int fd = open(card_path, O_RDONLY); if(fd == -1) { - fprintf(stderr, "gsr error: for_each_active_monitor_output_drm failed, failed to open \"%s\", error: %s\n", card_path, strerror(errno)); + gsr_log(GSR_LOG_LEVEL_ERROR, "for_each_active_monitor_output_drm failed, failed to open \"%s\", error: %s", card_path, strerror(errno)); return; } @@ -365,14 +366,14 @@ bool gl_get_gpu_info(gsr_egl *egl, gsr_gpu_info *info) { info->is_steam_deck = false; if(!gl_vendor) { - fprintf(stderr, "gsr error: failed to get gpu vendor\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "failed to get gpu vendor"); return false; } if(gl_renderer) { for(int i = 0; software_renderers[i]; ++i) { if(strstr(gl_renderer, software_renderers[i])) { - fprintf(stderr, "gsr error: your opengl environment is not properly setup. It's using %s (software rendering) for opengl instead of your graphics card. Please make sure your graphics driver is properly installed\n", software_renderers[i]); + gsr_log(GSR_LOG_LEVEL_ERROR, "your opengl environment is not properly setup. It's using %s (software rendering) for opengl instead of your graphics card. Please make sure your graphics driver is properly installed", software_renderers[i]); return false; } } @@ -389,7 +390,7 @@ bool gl_get_gpu_info(gsr_egl *egl, gsr_gpu_info *info) { else if(strstr(gl_vendor, "Broadcom")) info->vendor = GSR_GPU_VENDOR_BROADCOM; else { - fprintf(stderr, "gsr error: unknown gpu vendor: %s\n", gl_vendor); + gsr_log(GSR_LOG_LEVEL_ERROR, "unknown gpu vendor: %s", gl_vendor); return false; } @@ -656,7 +657,7 @@ bool get_nvidia_driver_version(int *major, int *minor) { FILE *f = fopen("/proc/driver/nvidia/version", "rb"); if(!f) { - fprintf(stderr, "gsr warning: failed to get nvidia driver version (failed to read /proc/driver/nvidia/version)\n"); + gsr_log(GSR_LOG_LEVEL_WARNING, "failed to get nvidia driver version (failed to read /proc/driver/nvidia/version)"); return false; } @@ -677,7 +678,7 @@ bool get_nvidia_driver_version(int *major, int *minor) { } if(!success) - fprintf(stderr, "gsr warning: failed to get nvidia driver version\n"); + gsr_log(GSR_LOG_LEVEL_WARNING, "failed to get nvidia driver version"); fclose(f); return success; diff --git a/src/wayland_host_bridge.c b/src/wayland_host_bridge.c index 4e8319b..f9299a4 100644 --- a/src/wayland_host_bridge.c +++ b/src/wayland_host_bridge.c @@ -1,4 +1,5 @@ #include "../include/wayland_host_bridge.h" +#include "../include/log.h" #include #include @@ -84,14 +85,14 @@ static struct wl_display* connect_via_bridge() { waitpid(pid, &status, 0); if(wayland_fd < 0) { - fprintf(stderr, "WaylandHostBridge: gsr-wayland-bridge did not return a wayland fd\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "WaylandHostBridge: gsr-wayland-bridge did not return a wayland fd"); return NULL; } struct wl_display *dpy = wl_display_connect_to_fd(wayland_fd); if(!dpy) { close(wayland_fd); - fprintf(stderr, "WaylandHostBridge: wl_display_connect_to_fd failed\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "WaylandHostBridge: wl_display_connect_to_fd failed"); return NULL; } return dpy; @@ -103,7 +104,7 @@ struct wl_display* wayland_connect_to_host() { struct wl_display *dpy = connect_via_bridge(); if(dpy) return dpy; - fprintf(stderr, "WaylandHostBridge: falling back to sandboxed wl_display_connect\n"); + gsr_log(GSR_LOG_LEVEL_WARNING, "WaylandHostBridge: falling back to sandboxed wl_display_connect"); } return wl_display_connect(NULL); } diff --git a/src/window/wayland.c b/src/window/wayland.c index 6c263c5..75e36f0 100644 --- a/src/window/wayland.c +++ b/src/window/wayland.c @@ -1,4 +1,5 @@ #include "../../include/window/wayland.h" +#include "../../include/log.h" #include "../../include/wayland_host_bridge.h" #include "../../include/vec2.h" @@ -234,12 +235,12 @@ static void registry_add_object(void *data, struct wl_registry *registry, uint32 window_wayland->compositor = wl_registry_bind(registry, name, &wl_compositor_interface, 1); } else if(strcmp(interface, wl_output_interface.name) == 0) { if(version < 4) { - fprintf(stderr, "gsr warning: wl output interface version is < 4, expected >= 4 to capture a monitor\n"); + gsr_log(GSR_LOG_LEVEL_WARNING, "wl output interface version is < 4, expected >= 4 to capture a monitor"); return; } if(window_wayland->num_outputs == GSR_MAX_OUTPUTS) { - fprintf(stderr, "gsr warning: reached maximum outputs (%d), ignoring output %u\n", GSR_MAX_OUTPUTS, name); + gsr_log(GSR_LOG_LEVEL_WARNING, "reached maximum outputs (%d), ignoring output %u", GSR_MAX_OUTPUTS, name); return; } @@ -257,7 +258,7 @@ static void registry_add_object(void *data, struct wl_registry *registry, uint32 wl_output_add_listener(gsr_output->output, &output_listener, gsr_output); } else if(strcmp(interface, zxdg_output_manager_v1_interface.name) == 0) { if(version < 1) { - fprintf(stderr, "gsr warning: xdg output interface version is < 1, expected >= 1 to capture a monitor\n"); + gsr_log(GSR_LOG_LEVEL_WARNING, "xdg output interface version is < 1, expected >= 1 to capture a monitor"); return; } @@ -326,7 +327,7 @@ static const struct zxdg_output_v1_listener xdg_output_listener = { static void gsr_window_wayland_set_monitor_outputs_from_xdg_output(gsr_window_wayland *self) { if(!self->xdg_output_manager) { - fprintf(stderr, "gsr warning: zxdg_output_manager not found. registered monitor positions might be incorrect\n"); + gsr_log(GSR_LOG_LEVEL_WARNING, "zxdg_output_manager not found. registered monitor positions might be incorrect"); return; } @@ -464,7 +465,7 @@ static void gsr_window_wayland_deinit(gsr_window_wayland *self) { static bool gsr_window_wayland_init(gsr_window_wayland *self) { self->display = wayland_connect_to_host(); if(!self->display) { - fprintf(stderr, "gsr error: gsr_window_wayland_init failed: failed to connect to the Wayland server\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_window_wayland_init failed: failed to connect to the Wayland server"); goto fail; } @@ -482,19 +483,19 @@ static bool gsr_window_wayland_init(gsr_window_wayland *self) { gsr_window_wayland_set_monitor_real_positions(self); if(!self->compositor) { - fprintf(stderr, "gsr error: gsr_window_wayland_init failed: failed to find compositor\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_window_wayland_init failed: failed to find compositor"); goto fail; } self->surface = wl_compositor_create_surface(self->compositor); if(!self->surface) { - fprintf(stderr, "gsr error: gsr_window_wayland_init failed: failed to create surface\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_window_wayland_init failed: failed to create surface"); goto fail; } self->window = wl_egl_window_create(self->surface, 16, 16); if(!self->window) { - fprintf(stderr, "gsr error: gsr_window_wayland_init failed: failed to create window\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_window_wayland_init failed: failed to create window"); goto fail; } diff --git a/src/window/x11.c b/src/window/x11.c index 3b3c955..b9b6cbf 100644 --- a/src/window/x11.c +++ b/src/window/x11.c @@ -1,4 +1,5 @@ #include "../../include/window/x11.h" +#include "../../include/log.h" #include "../../include/vec2.h" #include "../../include/defs.h" @@ -32,7 +33,7 @@ typedef struct { static void store_x11_monitor(const gsr_monitor *monitor, void *userdata) { gsr_window_x11 *window_x11 = userdata; if(window_x11->num_outputs == GSR_MAX_OUTPUTS) { - fprintf(stderr, "gsr warning: reached maximum outputs (%d), ignoring output %s\n", GSR_MAX_OUTPUTS, monitor->name); + gsr_log(GSR_LOG_LEVEL_WARNING, "reached maximum outputs (%d), ignoring output %s", GSR_MAX_OUTPUTS, monitor->name); return; } @@ -68,7 +69,7 @@ static void gsr_window_x11_deinit(gsr_window_x11 *self) { static bool gsr_window_x11_init(gsr_window_x11 *self) { self->window = XCreateWindow(self->display, DefaultRootWindow(self->display), 0, 0, 16, 16, 0, CopyFromParent, InputOutput, CopyFromParent, 0, NULL); if(!self->window) { - fprintf(stderr, "gsr error: gsr_window_x11_init failed: failed to create gl window\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_window_x11_init failed: failed to create gl window"); return false; } -- cgit v1.2.3 From 5611e6a7a70b79496da20ffd171d636c2d3c9065 Mon Sep 17 00:00:00 2001 From: dec05eba Date: Sat, 1 Aug 2026 19:13:38 +0200 Subject: Move string/date/array helpers from main.cpp to utils as reusable C functions --- include/utils.h | 18 ++ src/main.cpp | 476 +++++++++++++++++++++++---------------------------- src/pipewire_audio.c | 29 +--- src/utils.c | 86 ++++++++++ 4 files changed, 327 insertions(+), 282 deletions(-) diff --git a/include/utils.h b/include/utils.h index d2c3b7d..d1a2a3c 100644 --- a/include/utils.h +++ b/include/utils.h @@ -6,6 +6,7 @@ #include "../include/defs.h" #include #include +#include typedef struct AVCodecContext AVCodecContext; typedef struct AVFrame AVFrame; @@ -64,4 +65,21 @@ unsigned int gl_create_texture(gsr_egl *egl, int width, int height, int internal bool get_nvidia_driver_version(int *major, int *minor); +/* Return false from the callback to stop splitting */ +typedef bool (*gsr_string_split_callback)(const char *str, size_t size, void *userdata); +void gsr_string_split(const char *str, char delimiter, gsr_string_split_callback callback, void *userdata); + +bool gsr_string_starts_with(const char *str, size_t str_size, const char *substr); +bool gsr_string_ends_with(const char *str, const char *substr); + +bool gsr_string_to_int(const char *str, size_t size, int *number); +bool gsr_string_to_int64(const char *str, size_t size, int64_t *number); + +bool gsr_array_ensure_capacity(void **array, size_t num_items, size_t *capacity_items, size_t item_size); + +/* These write a formatted local date/time into |str|, which should be at least 128 bytes in size */ +void gsr_get_date_str(char *str, size_t size); +void gsr_get_date_only_str(char *str, size_t size); +void gsr_get_time_only_str(char *str, size_t size); + #endif /* GSR_UTILS_H */ diff --git a/src/main.cpp b/src/main.cpp index df609bb..a2933f3 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -781,30 +781,6 @@ static void save_replay_30_minutes_handler(int) { save_replay_seconds = 60*30; } -static std::string get_date_str() { - char str[128]; - time_t now = time(NULL); - struct tm *t = localtime(&now); - strftime(str, sizeof(str)-1, "%Y-%m-%d_%H-%M-%S", t); - return str; -} - -static std::string get_date_only_str() { - char str[128]; - time_t now = time(NULL); - struct tm *t = localtime(&now); - strftime(str, sizeof(str)-1, "%Y-%m-%d", t); - return str; -} - -static std::string get_time_only_str() { - char str[128]; - time_t now = time(NULL); - struct tm *t = localtime(&now); - strftime(str, sizeof(str)-1, "%H-%M-%S", t); - return str; -} - static AVStream* create_stream(AVFormatContext *av_format_context, AVCodecContext *codec_context) { AVStream *stream = avformat_new_stream(av_format_context, nullptr); if (!stream) { @@ -1082,16 +1058,20 @@ static std::future save_replay_thread; static std::string save_replay_output_filepath; static std::string create_new_recording_filepath_from_timestamp(std::string directory, const char *filename_prefix, const std::string &file_extension, bool date_folders) { + char date_str[128]; std::string output_filepath; if(date_folders) { - std::string output_folder = directory + '/' + get_date_only_str(); + gsr_get_date_only_str(date_str, sizeof(date_str)); + std::string output_folder = directory + '/' + date_str; if(create_directory_recursive(&output_folder[0]) != 0) gsr_log(GSR_LOG_LEVEL_ERROR, "failed to create directory: %s", output_folder.c_str()); - output_filepath = output_folder + "/" + filename_prefix + "_" + get_time_only_str() + "." + file_extension; + gsr_get_time_only_str(date_str, sizeof(date_str)); + output_filepath = output_folder + "/" + filename_prefix + "_" + date_str + "." + file_extension; } else { if(create_directory_recursive(&directory[0]) != 0) gsr_log(GSR_LOG_LEVEL_ERROR, "failed to create directory: %s", directory.c_str()); - output_filepath = directory + "/" + filename_prefix + "_" + get_date_str() + "." + file_extension; + gsr_get_date_str(date_str, sizeof(date_str)); + output_filepath = directory + "/" + filename_prefix + "_" + date_str + "." + file_extension; } return output_filepath; } @@ -1240,35 +1220,6 @@ static bool save_replay_async(AVCodecContext *video_codec_context, int video_str return true; } -static void split_string(const std::string &str, char delimiter, std::function callback) { - size_t index = 0; - while(index < str.size()) { - size_t end_index = str.find(delimiter, index); - if(end_index == std::string::npos) - end_index = str.size(); - - if(!callback(&str[index], end_index - index)) - break; - - index = end_index + 1; - } -} - -static bool string_starts_with(const char *str, size_t str_size, const char *substr) { - int len = strlen(substr); - return (int)str_size >= len && memcmp(str, substr, len) == 0; -} - -static bool string_starts_with(const std::string &str, const char *substr) { - return string_starts_with(str.data(), str.size(), substr); -} - -static bool string_ends_with(const char *str, const char *substr) { - int str_len = strlen(str); - int substr_len = strlen(substr); - return str_len >= substr_len && memcmp(str + str_len - substr_len, substr, substr_len) == 0; -} - static const AudioDevice* get_audio_device_by_name(const std::vector &audio_devices, const char *name) { for(const auto &audio_device : audio_devices) { if(strcmp(audio_device.name.c_str(), name) == 0) @@ -1277,44 +1228,42 @@ static const AudioDevice* get_audio_device_by_name(const std::vectorcustom_name = audio_input.name.substr(5); + } else if(gsr_string_starts_with(audio_input.name.data(), audio_input.name.size(), "app:")) { + audio_input.name.erase(audio_input.name.begin(), audio_input.name.begin() + 4); + audio_input.type = AudioInputType::APPLICATION; + audio_input.inverted = false; + result->audio_inputs.push_back(std::move(audio_input)); + } else if(gsr_string_starts_with(audio_input.name.data(), audio_input.name.size(), "app-inverse:")) { + audio_input.name.erase(audio_input.name.begin(), audio_input.name.begin() + 12); + audio_input.type = AudioInputType::APPLICATION; + audio_input.inverted = true; + result->audio_inputs.push_back(std::move(audio_input)); + } else if(gsr_string_starts_with(audio_input.name.data(), audio_input.name.size(), "device:")) { + audio_input.name.erase(audio_input.name.begin(), audio_input.name.begin() + 7); + audio_input.type = AudioInputType::DEVICE; + result->audio_inputs.push_back(std::move(audio_input)); + } else { + audio_input.type = AudioInputType::DEVICE; + result->audio_inputs.push_back(std::move(audio_input)); + } - if(string_starts_with(audio_input.name.c_str(), "name:")) { - result.custom_name = audio_input.name.substr(5); - return true; - } else if(string_starts_with(audio_input.name.c_str(), "app:")) { - audio_input.name.erase(audio_input.name.begin(), audio_input.name.begin() + 4); - audio_input.type = AudioInputType::APPLICATION; - audio_input.inverted = false; - result.audio_inputs.push_back(std::move(audio_input)); - return true; - } else if(string_starts_with(audio_input.name.c_str(), "app-inverse:")) { - audio_input.name.erase(audio_input.name.begin(), audio_input.name.begin() + 12); - audio_input.type = AudioInputType::APPLICATION; - audio_input.inverted = true; - result.audio_inputs.push_back(std::move(audio_input)); - return true; - } else if(string_starts_with(audio_input.name.c_str(), "device:")) { - audio_input.name.erase(audio_input.name.begin(), audio_input.name.begin() + 7); - audio_input.type = AudioInputType::DEVICE; - result.audio_inputs.push_back(std::move(audio_input)); - return true; - } else { - audio_input.type = AudioInputType::DEVICE; - result.audio_inputs.push_back(std::move(audio_input)); - return true; - } - }); + return true; +} +static MergedAudioInputs parse_audio_input_arg(const char *str) { + MergedAudioInputs result; + result.track_name = str; + gsr_string_split(str, '|', parse_audio_input_arg_callback, &result); return result; } @@ -2692,16 +2641,6 @@ static bool contains_non_hex_number(const char *str) { return is_hex && !hex_start; } -template -static bool string_to_int(const char *str, size_t len, T *number) { - char number_str[32]; - snprintf(number_str, sizeof(number_str), "%.*s", (int)len, str); - - errno = 0; - *number = strtol(number_str, NULL, 0); - return errno == 0; -} - static void capture_source_type_from_string(const char *capture_source_str, size_t size, CaptureSource &capture_source) { char capture_source_str_n[64]; snprintf(capture_source_str_n, sizeof(capture_source_str_n), "%.*s", (int)size, capture_source_str); @@ -2766,180 +2705,199 @@ static bool string_to_bool(const char *str, size_t len, bool *value) { } } -static void parse_capture_source_options(const std::string &capture_source_str, CaptureSource &capture_source) { - bool is_first_column = true; +struct ParseCaptureSourceOptionsUserdata { + CaptureSource *capture_source; + bool is_first_column; +}; - split_string(capture_source_str, ';', [&](const char *sub, size_t size) { - if(size == 0) - return true; +static bool parse_capture_source_options_callback(const char *sub, size_t size, void *userdata) { + ParseCaptureSourceOptionsUserdata *parse_userdata = (ParseCaptureSourceOptionsUserdata*)userdata; + CaptureSource &capture_source = *parse_userdata->capture_source; + if(size == 0) + return true; - // First column contains the capture target - if(is_first_column) { - is_first_column = false; - return true; - } + if(parse_userdata->is_first_column) { + parse_userdata->is_first_column = false; + return true; + } - if(string_starts_with(sub, size, "x=")) { - capture_source.pos.x_type = sub[size - 1] == '%' ? VVEC2I_TYPE_SCALAR : VVEC2I_TYPE_PIXELS; - sub += 2; - size -= 2; - if(!string_to_int(sub, size, &capture_source.pos.x)) { - gsr_log(GSR_LOG_LEVEL_ERROR, "invalid capture target value for option x: \"%.*s\", expected a number", (int)size, sub); - _exit(1); - } - } else if(string_starts_with(sub, size, "y=")) { - capture_source.pos.y_type = sub[size - 1] == '%' ? VVEC2I_TYPE_SCALAR : VVEC2I_TYPE_PIXELS; - sub += 2; - size -= 2; - if(!string_to_int(sub, size, &capture_source.pos.y)) { - gsr_log(GSR_LOG_LEVEL_ERROR, "invalid capture target value for option y: \"%.*s\", expected a number", (int)size, sub); - _exit(1); - } + if(gsr_string_starts_with(sub, size, "x=")) { + capture_source.pos.x_type = sub[size - 1] == '%' ? VVEC2I_TYPE_SCALAR : VVEC2I_TYPE_PIXELS; + sub += 2; + size -= 2; + if(!gsr_string_to_int(sub, size, &capture_source.pos.x)) { + gsr_log(GSR_LOG_LEVEL_ERROR, "invalid capture target value for option x: \"%.*s\", expected a number", (int)size, sub); + _exit(1); + } + } else if(gsr_string_starts_with(sub, size, "y=")) { + capture_source.pos.y_type = sub[size - 1] == '%' ? VVEC2I_TYPE_SCALAR : VVEC2I_TYPE_PIXELS; + sub += 2; + size -= 2; + if(!gsr_string_to_int(sub, size, &capture_source.pos.y)) { + gsr_log(GSR_LOG_LEVEL_ERROR, "invalid capture target value for option y: \"%.*s\", expected a number", (int)size, sub); + _exit(1); + } - } else if(string_starts_with(sub, size, "width=")) { - capture_source.size.x_type = sub[size - 1] == '%' ? VVEC2I_TYPE_SCALAR : VVEC2I_TYPE_PIXELS; - sub += 6; - size -= 6; - if(!string_to_int(sub, size, &capture_source.size.x)) { - gsr_log(GSR_LOG_LEVEL_ERROR, "invalid capture target value for option width: \"%.*s\", expected a number", (int)size, sub); - _exit(1); - } - } else if(string_starts_with(sub, size, "height=")) { - capture_source.size.y_type = sub[size - 1] == '%' ? VVEC2I_TYPE_SCALAR : VVEC2I_TYPE_PIXELS; - sub += 7; - size -= 7; - if(!string_to_int(sub, size, &capture_source.size.y)) { - gsr_log(GSR_LOG_LEVEL_ERROR, "invalid capture target value for option height: \"%.*s\", expected a number", (int)size, sub); - _exit(1); - } - } else if(string_starts_with(sub, size, "halign=")) { - sub += 7; - size -= 7; - if(!string_to_capture_alignment(sub, size, &capture_source.halign)) { - gsr_log(GSR_LOG_LEVEL_ERROR, "invalid capture target value for option halign: \"%.*s\", expected a \"start\", \"center\" or \"end\"", (int)size, sub); - _exit(1); - } - } else if(string_starts_with(sub, size, "valign=")) { - sub += 7; - size -= 7; - if(!string_to_capture_alignment(sub, size, &capture_source.valign)) { - gsr_log(GSR_LOG_LEVEL_ERROR, "invalid capture target value for option valign: \"%.*s\", expected a \"start\", \"center\" or \"end\"", (int)size, sub); - _exit(1); - } - } else if(string_starts_with(sub, size, "pixfmt=")) { - sub += 7; - size -= 7; - if(!string_to_v4l2_pixfmt(sub, size, &capture_source.v4l2_pixfmt)) { - gsr_log(GSR_LOG_LEVEL_ERROR, "invalid v4l2 pixfmt value for option pixfmt: \"%.*s\", expected a \"auto\", \"yuyv\" or \"mjpeg\"", (int)size, sub); - _exit(1); - } - } else if(string_starts_with(sub, size, "hflip=")) { - sub += 6; - size -= 6; - bool hflip = false; - if(!string_to_bool(sub, size, &hflip)) { - gsr_log(GSR_LOG_LEVEL_ERROR, "invalid bool value for option hflip: \"%.*s\", expected a \"true\" or \"false\"", (int)size, sub); - _exit(1); - } + } else if(gsr_string_starts_with(sub, size, "width=")) { + capture_source.size.x_type = sub[size - 1] == '%' ? VVEC2I_TYPE_SCALAR : VVEC2I_TYPE_PIXELS; + sub += 6; + size -= 6; + if(!gsr_string_to_int(sub, size, &capture_source.size.x)) { + gsr_log(GSR_LOG_LEVEL_ERROR, "invalid capture target value for option width: \"%.*s\", expected a number", (int)size, sub); + _exit(1); + } + } else if(gsr_string_starts_with(sub, size, "height=")) { + capture_source.size.y_type = sub[size - 1] == '%' ? VVEC2I_TYPE_SCALAR : VVEC2I_TYPE_PIXELS; + sub += 7; + size -= 7; + if(!gsr_string_to_int(sub, size, &capture_source.size.y)) { + gsr_log(GSR_LOG_LEVEL_ERROR, "invalid capture target value for option height: \"%.*s\", expected a number", (int)size, sub); + _exit(1); + } + } else if(gsr_string_starts_with(sub, size, "halign=")) { + sub += 7; + size -= 7; + if(!string_to_capture_alignment(sub, size, &capture_source.halign)) { + gsr_log(GSR_LOG_LEVEL_ERROR, "invalid capture target value for option halign: \"%.*s\", expected a \"start\", \"center\" or \"end\"", (int)size, sub); + _exit(1); + } + } else if(gsr_string_starts_with(sub, size, "valign=")) { + sub += 7; + size -= 7; + if(!string_to_capture_alignment(sub, size, &capture_source.valign)) { + gsr_log(GSR_LOG_LEVEL_ERROR, "invalid capture target value for option valign: \"%.*s\", expected a \"start\", \"center\" or \"end\"", (int)size, sub); + _exit(1); + } + } else if(gsr_string_starts_with(sub, size, "pixfmt=")) { + sub += 7; + size -= 7; + if(!string_to_v4l2_pixfmt(sub, size, &capture_source.v4l2_pixfmt)) { + gsr_log(GSR_LOG_LEVEL_ERROR, "invalid v4l2 pixfmt value for option pixfmt: \"%.*s\", expected a \"auto\", \"yuyv\" or \"mjpeg\"", (int)size, sub); + _exit(1); + } + } else if(gsr_string_starts_with(sub, size, "hflip=")) { + sub += 6; + size -= 6; + bool hflip = false; + if(!string_to_bool(sub, size, &hflip)) { + gsr_log(GSR_LOG_LEVEL_ERROR, "invalid bool value for option hflip: \"%.*s\", expected a \"true\" or \"false\"", (int)size, sub); + _exit(1); + } - if(hflip) - capture_source.flip |= GSR_FLIP_HORIZONTAL; - } else if(string_starts_with(sub, size, "vflip=")) { - sub += 6; - size -= 6; - bool vflip = false; - if(!string_to_bool(sub, size, &vflip)) { - gsr_log(GSR_LOG_LEVEL_ERROR, "invalid bool value for option vflip: \"%.*s\", expected a \"true\" or \"false\"", (int)size, sub); - _exit(1); - } + if(hflip) + capture_source.flip |= GSR_FLIP_HORIZONTAL; + } else if(gsr_string_starts_with(sub, size, "vflip=")) { + sub += 6; + size -= 6; + bool vflip = false; + if(!string_to_bool(sub, size, &vflip)) { + gsr_log(GSR_LOG_LEVEL_ERROR, "invalid bool value for option vflip: \"%.*s\", expected a \"true\" or \"false\"", (int)size, sub); + _exit(1); + } - if(vflip) - capture_source.flip |= GSR_FLIP_VERTICAL; - } else if(string_starts_with(sub, size, "camera_fps=")) { - sub += 11; - size -= 11; - if(!string_to_int(sub, size, &capture_source.camera_fps)) { - gsr_log(GSR_LOG_LEVEL_ERROR, "invalid capture target value for option camera_fps: \"%.*s\", expected a number", (int)size, sub); - _exit(1); - } - } else if(string_starts_with(sub, size, "camera_width=")) { - sub += 13; - size -= 13; - if(!string_to_int(sub, size, &capture_source.camera_resolution.x)) { - gsr_log(GSR_LOG_LEVEL_ERROR, "invalid capture target value for option camera_width: \"%.*s\", expected a number", (int)size, sub); - _exit(1); - } - } else if(string_starts_with(sub, size, "camera_height=")) { - sub += 14; - size -= 14; - if(!string_to_int(sub, size, &capture_source.camera_resolution.y)) { - gsr_log(GSR_LOG_LEVEL_ERROR, "invalid capture target value for option camera_height: \"%.*s\", expected a number", (int)size, sub); - _exit(1); - } - } else { - gsr_log(GSR_LOG_LEVEL_ERROR, "invalid capture target option \"%.*s\", expected x, y, width, height, halign, valign, pixfmt, hflip, vflip, camera_fps, camera_width or camera_height", (int)size, sub); - _exit(1); + if(vflip) + capture_source.flip |= GSR_FLIP_VERTICAL; + } else if(gsr_string_starts_with(sub, size, "camera_fps=")) { + sub += 11; + size -= 11; + if(!gsr_string_to_int(sub, size, &capture_source.camera_fps)) { + gsr_log(GSR_LOG_LEVEL_ERROR, "invalid capture target value for option camera_fps: \"%.*s\", expected a number", (int)size, sub); + _exit(1); + } + } else if(gsr_string_starts_with(sub, size, "camera_width=")) { + sub += 13; + size -= 13; + if(!gsr_string_to_int(sub, size, &capture_source.camera_resolution.x)) { + gsr_log(GSR_LOG_LEVEL_ERROR, "invalid capture target value for option camera_width: \"%.*s\", expected a number", (int)size, sub); + _exit(1); } + } else if(gsr_string_starts_with(sub, size, "camera_height=")) { + sub += 14; + size -= 14; + if(!gsr_string_to_int(sub, size, &capture_source.camera_resolution.y)) { + gsr_log(GSR_LOG_LEVEL_ERROR, "invalid capture target value for option camera_height: \"%.*s\", expected a number", (int)size, sub); + _exit(1); + } + } else { + gsr_log(GSR_LOG_LEVEL_ERROR, "invalid capture target option \"%.*s\", expected x, y, width, height, halign, valign, pixfmt, hflip, vflip, camera_fps, camera_width or camera_height", (int)size, sub); + _exit(1); + } - return true; - }); + return true; } -static std::vector parse_capture_source_arg(const char *capture_source_arg, const args_parser &arg_parser) { - std::vector requested_capture_sources; - const bool has_multiple_capture_sources = strchr(capture_source_arg, '|') != nullptr; +static void parse_capture_source_options(const std::string &capture_source_str, CaptureSource &capture_source) { + ParseCaptureSourceOptionsUserdata userdata; + userdata.capture_source = &capture_source; + userdata.is_first_column = true; + gsr_string_split(capture_source_str.c_str(), ';', parse_capture_source_options_callback, &userdata); +} - split_string(capture_source_arg, '|', [&](const char *sub, size_t size) { - if(size == 0) - return true; +struct ParseCaptureSourceArgUserdata { + std::vector *requested_capture_sources; + const args_parser *arg_parser; + bool has_multiple_capture_sources; +}; - const char *substr_start = sub; - size_t capture_source_size = size; - const char *capture_source_end = (const char*)memchr(sub, ';', size); - if(capture_source_end) - capture_source_size = capture_source_end - sub; - - CaptureSource capture_source; - capture_source.region_pos = arg_parser.region_position; - capture_source.region_size = arg_parser.region_size; - - if(string_starts_with(sub, capture_source_size, "monitor:")) { - capture_source.type = GSR_CAPTURE_SOURCE_TYPE_MONITOR; - sub += 8; - capture_source_size -= 8; - } else if(string_starts_with(sub, capture_source_size, "window:")) { - capture_source.type = GSR_CAPTURE_SOURCE_TYPE_WINDOW; - sub += 7; - capture_source_size -= 7; - } else if(string_starts_with(sub, capture_source_size, "v4l2:")) { - capture_source.type = GSR_CAPTURE_SOURCE_TYPE_V4L2; - sub += 5; - capture_source_size -= 5; - } else { - capture_source_type_from_string(sub, capture_source_size, capture_source); - } +static bool parse_capture_source_arg_callback(const char *sub, size_t size, void *userdata) { + ParseCaptureSourceArgUserdata *parse_userdata = (ParseCaptureSourceArgUserdata*)userdata; + if(size == 0) + return true; - capture_source.name.assign(sub, capture_source_size); + const char *substr_start = sub; + size_t capture_source_size = size; + const char *capture_source_end = (const char*)memchr(sub, ';', size); + if(capture_source_end) + capture_source_size = capture_source_end - sub; - if(capture_source.type == GSR_CAPTURE_SOURCE_TYPE_WINDOW) { - if(!string_to_int(capture_source.name.c_str(), capture_source.name.size(), &capture_source.window_id)) { - gsr_log(GSR_LOG_LEVEL_ERROR, "invalid window number %s", capture_source.name.c_str()); - args_parser_print_usage(); - _exit(1); - } - } + CaptureSource capture_source; + capture_source.region_pos = parse_userdata->arg_parser->region_position; + capture_source.region_size = parse_userdata->arg_parser->region_size; + + if(gsr_string_starts_with(sub, capture_source_size, "monitor:")) { + capture_source.type = GSR_CAPTURE_SOURCE_TYPE_MONITOR; + sub += 8; + capture_source_size -= 8; + } else if(gsr_string_starts_with(sub, capture_source_size, "window:")) { + capture_source.type = GSR_CAPTURE_SOURCE_TYPE_WINDOW; + sub += 7; + capture_source_size -= 7; + } else if(gsr_string_starts_with(sub, capture_source_size, "v4l2:")) { + capture_source.type = GSR_CAPTURE_SOURCE_TYPE_V4L2; + sub += 5; + capture_source_size -= 5; + } else { + capture_source_type_from_string(sub, capture_source_size, capture_source); + } - if(has_multiple_capture_sources) { - capture_source.halign = GSR_CAPTURE_ALIGN_START; - capture_source.valign = GSR_CAPTURE_ALIGN_START; - capture_source.pos = {0, 0, VVEC2I_TYPE_PIXELS, VVEC2I_TYPE_PIXELS}; + capture_source.name.assign(sub, capture_source_size); + + if(capture_source.type == GSR_CAPTURE_SOURCE_TYPE_WINDOW) { + if(!gsr_string_to_int64(capture_source.name.c_str(), capture_source.name.size(), &capture_source.window_id)) { + gsr_log(GSR_LOG_LEVEL_ERROR, "invalid window number %s", capture_source.name.c_str()); + args_parser_print_usage(); + _exit(1); } + } - parse_capture_source_options(std::string(substr_start, size), capture_source); - requested_capture_sources.push_back(capture_source); - return true; - }); + if(parse_userdata->has_multiple_capture_sources) { + capture_source.halign = GSR_CAPTURE_ALIGN_START; + capture_source.valign = GSR_CAPTURE_ALIGN_START; + capture_source.pos = {0, 0, VVEC2I_TYPE_PIXELS, VVEC2I_TYPE_PIXELS}; + } + + parse_capture_source_options(std::string(substr_start, size), capture_source); + parse_userdata->requested_capture_sources->push_back(capture_source); + return true; +} +static std::vector parse_capture_source_arg(const char *capture_source_arg, const args_parser &arg_parser) { + std::vector requested_capture_sources; + ParseCaptureSourceArgUserdata userdata; + userdata.requested_capture_sources = &requested_capture_sources; + userdata.arg_parser = &arg_parser; + userdata.has_multiple_capture_sources = strchr(capture_source_arg, '|') != nullptr; + gsr_string_split(capture_source_arg, '|', parse_capture_source_arg_callback, &userdata); return requested_capture_sources; } @@ -3462,10 +3420,10 @@ static AudioDeviceData create_application_audio_audio_input(const MergedAudioInp #endif static bool get_image_format_from_filename(const char *filename, gsr_image_format *image_format) { - if(string_ends_with(filename, ".jpg") || string_ends_with(filename, ".jpeg")) { + if(gsr_string_ends_with(filename, ".jpg") || gsr_string_ends_with(filename, ".jpeg")) { *image_format = GSR_IMAGE_FORMAT_JPEG; return true; - } else if(string_ends_with(filename, ".png")) { + } else if(gsr_string_ends_with(filename, ".png")) { *image_format = GSR_IMAGE_FORMAT_PNG; return true; } else { diff --git a/src/pipewire_audio.c b/src/pipewire_audio.c index be1ba21..73192a9 100644 --- a/src/pipewire_audio.c +++ b/src/pipewire_audio.c @@ -1,5 +1,6 @@ #include "../include/pipewire_audio.h" #include "../include/log.h" +#include "../include/utils.h" #include #include @@ -400,24 +401,6 @@ static bool gsr_pipewire_audio_listen_on_metadata(gsr_pipewire_audio *self, uint return true; } -static bool array_ensure_capacity(void **array, size_t size, size_t *capacity_items, size_t element_size) { - if(size + 1 >= *capacity_items) { - size_t new_capacity_items = *capacity_items * 2; - if(new_capacity_items == 0) - new_capacity_items = 32; - - void *new_data = realloc(*array, new_capacity_items * element_size); - if(!new_data) { - gsr_log(GSR_LOG_LEVEL_ERROR, "pipewire_audio: failed to reallocate memory"); - return false; - } - - *array = new_data; - *capacity_items = new_capacity_items; - } - return true; -} - struct gsr_pipewire_audio_node_binding { gsr_pipewire_audio *self; struct pw_proxy *proxy; @@ -435,7 +418,7 @@ static gsr_pipewire_audio_node* gsr_pipewire_audio_get_node_by_id(gsr_pipewire_a } static void gsr_pipewire_audio_add_node(gsr_pipewire_audio *self, uint32_t id, const char *node_name, gsr_pipewire_audio_node_type type, bool is_virtual, bool is_application) { - if(!array_ensure_capacity((void**)&self->stream_nodes, self->num_stream_nodes, &self->stream_nodes_capacity_items, sizeof(gsr_pipewire_audio_node))) + if(!gsr_array_ensure_capacity((void**)&self->stream_nodes, self->num_stream_nodes, &self->stream_nodes_capacity_items, sizeof(gsr_pipewire_audio_node))) return; char *node_name_copy = strdup(node_name); @@ -525,7 +508,7 @@ static const struct pw_proxy_events node_proxy_events = { /* The registry only broadcasts a small set of the node properties (node.name, media.class, application.name, etc). Binding to the node is required to get the other properties (node.virtual, application.id, etc) from the node info event. */ static void gsr_pipewire_audio_bind_node(gsr_pipewire_audio *self, uint32_t id) { - if(!array_ensure_capacity((void**)&self->node_bindings, self->num_node_bindings, &self->node_bindings_capacity_items, sizeof(gsr_pipewire_audio_node_binding*))) + if(!gsr_array_ensure_capacity((void**)&self->node_bindings, self->num_node_bindings, &self->node_bindings_capacity_items, sizeof(gsr_pipewire_audio_node_binding*))) return; gsr_pipewire_audio_node_binding *node_binding = calloc(1, sizeof(gsr_pipewire_audio_node_binding)); @@ -592,7 +575,7 @@ static void registry_event_global(void *data, uint32_t id, uint32_t permissions, const int node_id_num = node_id ? atoi(node_id) : 0; if(port_name && direction >= 0 && node_id_num > 0) { - if(!array_ensure_capacity((void**)&self->ports, self->num_ports, &self->ports_capacity_items, sizeof(gsr_pipewire_audio_port))) + if(!gsr_array_ensure_capacity((void**)&self->ports, self->num_ports, &self->ports_capacity_items, sizeof(gsr_pipewire_audio_port))) return; //fprintf(stderr, " port name: %s, node id: %d, direction: %s\n", port_name, node_id_num, port_direction); @@ -615,7 +598,7 @@ static void registry_event_global(void *data, uint32_t id, uint32_t permissions, const uint32_t output_node_id_num = output_node ? atoi(output_node) : 0; const uint32_t input_node_id_num = input_node ? atoi(input_node) : 0; if(output_node_id_num > 0 && input_node_id_num > 0) { - if(!array_ensure_capacity((void**)&self->links, self->num_links, &self->links_capacity_items, sizeof(gsr_pipewire_audio_link))) + if(!gsr_array_ensure_capacity((void**)&self->links, self->num_links, &self->links_capacity_items, sizeof(gsr_pipewire_audio_link))) return; //fprintf(stderr, " new link (%u): %u -> %u\n", id, output_node_id_num, input_node_id_num); @@ -891,7 +874,7 @@ static bool string_remove_suffix(char *str, const char *suffix) { } static bool gsr_pipewire_audio_add_links_to_output(gsr_pipewire_audio *self, const char **output_names, int num_output_names, const char *input_name, gsr_pipewire_audio_node_type output_type, gsr_pipewire_audio_link_input_type input_type, bool inverted) { - if(!array_ensure_capacity((void**)&self->requested_links, self->num_requested_links, &self->requested_links_capacity_items, sizeof(gsr_pipewire_audio_requested_link))) + if(!gsr_array_ensure_capacity((void**)&self->requested_links, self->num_requested_links, &self->requested_links_capacity_items, sizeof(gsr_pipewire_audio_requested_link))) return false; gsr_pipewire_audio_requested_output *outputs = calloc(num_output_names, sizeof(gsr_pipewire_audio_requested_output)); diff --git a/src/utils.c b/src/utils.c index 75a6427..9ae34d7 100644 --- a/src/utils.c +++ b/src/utils.c @@ -683,3 +683,89 @@ bool get_nvidia_driver_version(int *major, int *minor) { fclose(f); return success; } + +void gsr_string_split(const char *str, char delimiter, gsr_string_split_callback callback, void *userdata) { + const size_t str_len = strlen(str); + size_t index = 0; + while(index < str_len) { + const char *end = strchr(str + index, delimiter); + const size_t end_index = end ? (size_t)(end - str) : str_len; + + if(!callback(str + index, end_index - index, userdata)) + break; + + index = end_index + 1; + } +} + +bool gsr_string_starts_with(const char *str, size_t str_size, const char *substr) { + const size_t substr_len = strlen(substr); + return str_size >= substr_len && memcmp(str, substr, substr_len) == 0; +} + +bool gsr_string_ends_with(const char *str, const char *substr) { + const size_t str_len = strlen(str); + const size_t substr_len = strlen(substr); + return str_len >= substr_len && memcmp(str + str_len - substr_len, substr, substr_len) == 0; +} + +static bool string_to_long(const char *str, size_t size, long *number) { + char number_str[32]; + snprintf(number_str, sizeof(number_str), "%.*s", (int)size, str); + + errno = 0; + *number = strtol(number_str, NULL, 0); + return errno == 0; +} + +bool gsr_string_to_int(const char *str, size_t size, int *number) { + long value = 0; + if(!string_to_long(str, size, &value)) + return false; + *number = value; + return true; +} + +bool gsr_string_to_int64(const char *str, size_t size, int64_t *number) { + long value = 0; + if(!string_to_long(str, size, &value)) + return false; + *number = value; + return true; +} + +bool gsr_array_ensure_capacity(void **array, size_t num_items, size_t *capacity_items, size_t item_size) { + if(num_items + 1 >= *capacity_items) { + size_t new_capacity_items = *capacity_items * 2; + if(new_capacity_items == 0) + new_capacity_items = 32; + + void *new_data = realloc(*array, new_capacity_items * item_size); + if(!new_data) { + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_array_ensure_capacity: failed to reallocate memory"); + return false; + } + + *array = new_data; + *capacity_items = new_capacity_items; + } + return true; +} + +void gsr_get_date_str(char *str, size_t size) { + time_t now = time(NULL); + struct tm *t = localtime(&now); + strftime(str, size - 1, "%Y-%m-%d_%H-%M-%S", t); +} + +void gsr_get_date_only_str(char *str, size_t size) { + time_t now = time(NULL); + struct tm *t = localtime(&now); + strftime(str, size - 1, "%Y-%m-%d", t); +} + +void gsr_get_time_only_str(char *str, size_t size) { + time_t now = time(NULL); + struct tm *t = localtime(&now); + strftime(str, size - 1, "%H-%M-%S", t); +} -- cgit v1.2.3 From 2b47b9926966b6e81ffda188193eb80653c6f14f Mon Sep 17 00:00:00 2001 From: dec05eba Date: Sat, 1 Aug 2026 21:07:12 +0200 Subject: Rewrite sound.cpp in C as sound.c --- include/sound.h | 84 ++++++ include/sound.hpp | 91 ------- meson.build | 2 +- src/main.cpp | 85 +++--- src/sound.c | 778 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/sound.cpp | 738 --------------------------------------------------- 6 files changed, 917 insertions(+), 861 deletions(-) create mode 100644 include/sound.h delete mode 100644 include/sound.hpp create mode 100644 src/sound.c delete mode 100644 src/sound.cpp diff --git a/include/sound.h b/include/sound.h new file mode 100644 index 0000000..1ec8940 --- /dev/null +++ b/include/sound.h @@ -0,0 +1,84 @@ +/* + Copyright (C) 2020 dec05eba + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#ifndef GSR_SOUND_H +#define GSR_SOUND_H + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct { + void *handle; + unsigned int frames; +} SoundDevice; + +typedef struct { + char name[256]; + char description[256]; +} gsr_audio_device; + +typedef struct { + char default_output[256]; + char default_input[256]; + gsr_audio_device *items; + size_t num_items; + size_t capacity_items; +} gsr_audio_devices; + +typedef enum { + GSR_AUDIO_FORMAT_S16, + GSR_AUDIO_FORMAT_S32, + GSR_AUDIO_FORMAT_F32 +} gsr_audio_format; + +/* + Get a sound device by name, returning the device into the |device| parameter. + |device_name| can be a device name or "default_output", "default_input" or "" to not connect to any device (used for app audio for example). + If the device name is "default_output" or "default_input" then it will automatically switch which + device is records from when the default output/input is changed in the system audio settings. + Returns 0 on success, or a negative value on failure. +*/ +int sound_device_get_by_name(SoundDevice *device, const char *node_name, const char *device_name, const char *description, unsigned int num_channels, unsigned int period_frame_size, gsr_audio_format audio_format); + +void sound_device_close(SoundDevice *device); + +/* + Discards the audio that has been captured so far. + Call this before the first call to sound_device_read_next_chunk to not get audio that was captured before that point, + since the sound device can be created a while before audio capture starts. +*/ +void sound_device_flush(SoundDevice *device); + +/* + Returns the next chunk of audio into @buffer. + Returns the number of frames read, or a negative value on failure. +*/ +int sound_device_read_next_chunk(SoundDevice *device, void **buffer, double timeout_sec, double *latency_seconds); + +void get_pulseaudio_inputs(gsr_audio_devices *audio_devices); +void gsr_audio_devices_deinit(gsr_audio_devices *self); +bool pulseaudio_server_is_pipewire(void); + +#ifdef __cplusplus +} +#endif + +#endif /* GSR_SOUND_H */ diff --git a/include/sound.hpp b/include/sound.hpp deleted file mode 100644 index d78e4b6..0000000 --- a/include/sound.hpp +++ /dev/null @@ -1,91 +0,0 @@ -/* - Copyright (C) 2020 dec05eba - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . -*/ - -#ifndef GPU_SCREEN_RECORDER_H -#define GPU_SCREEN_RECORDER_H - -#include -#include -#include - -typedef struct { - void *handle; - unsigned int frames; -} SoundDevice; - -struct AudioDevice { - std::string name; - std::string description; -}; - -struct AudioDevices { - std::string default_output; - std::string default_input; - std::vector audio_inputs; -}; - -enum class AudioInputType { - DEVICE, - APPLICATION -}; - -struct AudioInput { - std::string name; - AudioInputType type = AudioInputType::DEVICE; - bool inverted = false; -}; - -struct MergedAudioInputs { - std::optional custom_name; - std::string track_name; - std::vector audio_inputs; -}; - -typedef enum { - S16, - S32, - F32 -} AudioFormat; - -/* - Get a sound device by name, returning the device into the |device| parameter. - |device_name| can be a device name or "default_output", "default_input" or "" to not connect to any device (used for app audio for example). - If the device name is "default_output" or "default_input" then it will automatically switch which - device is records from when the default output/input is changed in the system audio settings. - Returns 0 on success, or a negative value on failure. -*/ -int sound_device_get_by_name(SoundDevice *device, const char *node_name, const char *device_name, const char *description, unsigned int num_channels, unsigned int period_frame_size, AudioFormat audio_format); - -void sound_device_close(SoundDevice *device); - -/* - Discards the audio that has been captured so far. - Call this before the first call to sound_device_read_next_chunk to not get audio that was captured before that point, - since the sound device can be created a while before audio capture starts. -*/ -void sound_device_flush(SoundDevice *device); - -/* - Returns the next chunk of audio into @buffer. - Returns the number of frames read, or a negative value on failure. -*/ -int sound_device_read_next_chunk(SoundDevice *device, void **buffer, double timeout_sec, double *latency_seconds); - -AudioDevices get_pulseaudio_inputs(); -bool pulseaudio_server_is_pipewire(); - -#endif /* GPU_SCREEN_RECORDER_H */ diff --git a/meson.build b/meson.build index 2bd947e..ebe8440 100644 --- a/meson.build +++ b/meson.build @@ -46,7 +46,7 @@ src = [ 'src/defs.c', 'src/plugins.c', 'src/wayland_host_bridge.c', - 'src/sound.cpp', + 'src/sound.c', 'src/main.cpp', ] diff --git a/src/main.cpp b/src/main.cpp index a2933f3..6862b99 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -36,6 +36,8 @@ extern "C" { #include #include #include +#include +#include #include #include #include @@ -46,7 +48,7 @@ extern "C" { #include #include -#include "../include/sound.hpp" +#include "../include/sound.h" extern "C" { #include @@ -68,6 +70,23 @@ extern "C" { #define GSR_VERSION "unknown" #endif +enum class AudioInputType { + DEVICE, + APPLICATION +}; + +struct AudioInput { + std::string name; + AudioInputType type = AudioInputType::DEVICE; + bool inverted = false; +}; + +struct MergedAudioInputs { + std::optional custom_name; + std::string track_name; + std::vector audio_inputs; +}; + // TODO: If options are not supported then they are returned (allocated) in the options. This should be free'd. // TODO: Remove LIBAVUTIL_VERSION_MAJOR checks in the future when ubuntu, pop os LTS etc update ffmpeg to >= 5.0 @@ -227,21 +246,21 @@ static int64_t audio_codec_get_get_bitrate(gsr_audio_codec audio_codec) { return 128000; } -static AudioFormat audio_codec_context_get_audio_format(const AVCodecContext *audio_codec_context) { +static gsr_audio_format audio_codec_context_get_audio_format(const AVCodecContext *audio_codec_context) { switch(audio_codec_context->sample_fmt) { - case AV_SAMPLE_FMT_FLT: return F32; - case AV_SAMPLE_FMT_FLTP: return S32; - case AV_SAMPLE_FMT_S16: return S16; - case AV_SAMPLE_FMT_S32: return S32; - default: return S16; + case AV_SAMPLE_FMT_FLT: return GSR_AUDIO_FORMAT_F32; + case AV_SAMPLE_FMT_FLTP: return GSR_AUDIO_FORMAT_S32; + case AV_SAMPLE_FMT_S16: return GSR_AUDIO_FORMAT_S16; + case AV_SAMPLE_FMT_S32: return GSR_AUDIO_FORMAT_S32; + default: return GSR_AUDIO_FORMAT_S16; } } -static AVSampleFormat audio_format_to_sample_format(const AudioFormat audio_format) { +static AVSampleFormat audio_format_to_sample_format(const gsr_audio_format audio_format) { switch(audio_format) { - case S16: return AV_SAMPLE_FMT_S16; - case S32: return AV_SAMPLE_FMT_S32; - case F32: return AV_SAMPLE_FMT_FLT; + case GSR_AUDIO_FORMAT_S16: return AV_SAMPLE_FMT_S16; + case GSR_AUDIO_FORMAT_S32: return AV_SAMPLE_FMT_S32; + case GSR_AUDIO_FORMAT_F32: return AV_SAMPLE_FMT_FLT; } assert(false); return AV_SAMPLE_FMT_S16; @@ -1220,10 +1239,10 @@ static bool save_replay_async(AVCodecContext *video_codec_context, int video_str return true; } -static const AudioDevice* get_audio_device_by_name(const std::vector &audio_devices, const char *name) { - for(const auto &audio_device : audio_devices) { - if(strcmp(audio_device.name.c_str(), name) == 0) - return &audio_device; +static const gsr_audio_device* get_audio_device_by_name(const gsr_audio_devices *audio_devices, const char *name) { + for(size_t i = 0; i < audio_devices->num_items; ++i) { + if(strcmp(audio_devices->items[i].name, name) == 0) + return &audio_devices->items[i]; } return nullptr; } @@ -1840,18 +1859,20 @@ static void info_command(void *userdata) { static void list_audio_devices_command(void *userdata) { (void)userdata; - const AudioDevices audio_devices = get_pulseaudio_inputs(); + gsr_audio_devices audio_devices; + get_pulseaudio_inputs(&audio_devices); - if(!audio_devices.default_output.empty()) + if(audio_devices.default_output[0] != '\0') puts("default_output|Default output"); - if(!audio_devices.default_input.empty()) + if(audio_devices.default_input[0] != '\0') puts("default_input|Default input"); - for(const auto &audio_input : audio_devices.audio_inputs) { - printf("%s|%s\n", audio_input.name.c_str(), audio_input.description.c_str()); + 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); _exit(0); } @@ -2549,7 +2570,7 @@ struct AudioTrackDescription { // Manually check if the audio inputs we give exist. This is only needed for pipewire, not pulseaudio. // Pipewire instead DEFAULTS TO THE DEFAULT AUDIO INPUT. THAT'S RETARDED. // OH, YOU MISSPELLED THE AUDIO INPUT? FUCK YOU -static std::vector parse_audio_inputs(const AudioDevices &audio_devices, const Arg *audio_input_arg) { +static std::vector parse_audio_inputs(const gsr_audio_devices *audio_devices, const Arg *audio_input_arg) { std::vector requested_audio_inputs; for(int i = 0; i < audio_input_arg->num_values; ++i) { @@ -2574,21 +2595,21 @@ static std::vector parse_audio_inputs(const AudioDevices &aud bool match = false; if(request_audio_input.name == "default_output") { - if(audio_devices.default_output.empty()) { + if(audio_devices->default_output[0] == '\0') { gsr_log(GSR_LOG_LEVEL_ERROR, "-a default_output was specified but no default audio output is specified in the audio server"); _exit(2); } match = true; audio_track_description.devices.push_back("Default output"); } else if(request_audio_input.name == "default_input") { - if(audio_devices.default_input.empty()) { + if(audio_devices->default_input[0] == '\0') { gsr_log(GSR_LOG_LEVEL_ERROR, "-a default_input was specified but no default audio input is specified in the audio server"); _exit(2); } match = true; audio_track_description.devices.push_back("Default input"); } else { - const AudioDevice *audio_device = get_audio_device_by_name(audio_devices.audio_inputs, request_audio_input.name.c_str()); + const gsr_audio_device *audio_device = get_audio_device_by_name(audio_devices, request_audio_input.name.c_str()); if(audio_device) { match = true; audio_track_description.devices.push_back(audio_device->description); @@ -2597,12 +2618,12 @@ static std::vector parse_audio_inputs(const AudioDevices &aud if(!match) { gsr_log(GSR_LOG_LEVEL_ERROR, "Audio device '%s' is not a valid audio device, expected one of:", request_audio_input.name.c_str()); - if(!audio_devices.default_output.empty()) + if(audio_devices->default_output[0] != '\0') fprintf(stderr, " default_output (Default output)\n"); - if(!audio_devices.default_input.empty()) + if(audio_devices->default_input[0] != '\0') fprintf(stderr, " default_input (Default input)\n"); - for(const auto &audio_device_input : audio_devices.audio_inputs) { - fprintf(stderr, " %s (%s)\n", audio_device_input.name.c_str(), audio_device_input.description.c_str()); + for(size_t j = 0; j < audio_devices->num_items; ++j) { + fprintf(stderr, " %s (%s)\n", audio_devices->items[j].name, audio_devices->items[j].description); } _exit(50); } @@ -3671,11 +3692,13 @@ int main(int argc, char **argv) { const Arg *audio_input_arg = args_parser_get_arg(&arg_parser, "-a"); assert(audio_input_arg); - AudioDevices audio_devices; + gsr_audio_devices audio_devices; + memset(&audio_devices, 0, sizeof(audio_devices)); if(audio_input_arg->num_values > 0) - audio_devices = get_pulseaudio_inputs(); + get_pulseaudio_inputs(&audio_devices); - std::vector requested_audio_inputs = parse_audio_inputs(audio_devices, audio_input_arg); + std::vector requested_audio_inputs = parse_audio_inputs(&audio_devices, audio_input_arg); + gsr_audio_devices_deinit(&audio_devices); const bool uses_app_audio = merged_audio_inputs_has_app_audio(requested_audio_inputs); std::vector app_audio_names; diff --git a/src/sound.c b/src/sound.c new file mode 100644 index 0000000..a01718b --- /dev/null +++ b/src/sound.c @@ -0,0 +1,778 @@ +#include "../include/sound.h" +#include "../include/log.h" +#include "../include/utils.h" + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#define RECONNECT_TRY_TIMEOUT_SECONDS 0.5 +#define DEVICE_NAME_MAX_SIZE 128 + +#define CHECK_DEAD_GOTO(p, rerror, label) \ + do { \ + if (!(p)->context || !PA_CONTEXT_IS_GOOD(pa_context_get_state((p)->context)) || \ + !(p)->stream || !PA_STREAM_IS_GOOD(pa_stream_get_state((p)->stream))) { \ + if (((p)->context && pa_context_get_state((p)->context) == PA_CONTEXT_FAILED) || \ + ((p)->stream && pa_stream_get_state((p)->stream) == PA_STREAM_FAILED)) { \ + if (rerror) \ + *(rerror) = pa_context_errno((p)->context); \ + } else \ + if (rerror) \ + *(rerror) = PA_ERR_BADSTATE; \ + goto label; \ + } \ + } while(false); + +typedef enum { + DEVICE_TYPE_STANDARD, + DEVICE_TYPE_DEFAULT_OUTPUT, + DEVICE_TYPE_DEFAULT_INPUT +} DeviceType; + +typedef struct { + pa_context *context; + pa_stream *stream; + pa_mainloop *mainloop; + + const void *read_data; + size_t read_index, read_length; + + uint8_t *output_data; + size_t output_index, output_length; + + int operation_success; + double latency_seconds; + + pa_buffer_attr attr; + pa_sample_spec ss; + + pthread_mutex_t reconnect_mutex; + bool reconnect_mutex_initialized; + DeviceType device_type; + char stream_name[256]; + char node_name[256]; + bool reconnect; + double reconnect_last_tried_seconds; + + char device_name[DEVICE_NAME_MAX_SIZE]; + char default_output_device_name[DEVICE_NAME_MAX_SIZE]; + char default_input_device_name[DEVICE_NAME_MAX_SIZE]; + + pa_proplist *proplist; + bool connected; +} pa_handle; + +static void pa_sound_device_free(pa_handle *p) { + assert(p); + + if(p->proplist) { + pa_proplist_free(p->proplist); + p->proplist = NULL; + } + + if (p->stream) { + pa_stream_unref(p->stream); + p->stream = NULL; + } + + if (p->context) { + pa_context_disconnect(p->context); + pa_context_unref(p->context); + p->context = NULL; + } + + if (p->mainloop) { + pa_mainloop_free(p->mainloop); + p->mainloop = NULL; + } + + if (p->output_data) { + free(p->output_data); + p->output_data = NULL; + } + + if (p->reconnect_mutex_initialized) { + pthread_mutex_destroy(&p->reconnect_mutex); + p->reconnect_mutex_initialized = false; + } + + pa_xfree(p); +} + +static void subscribe_update_default_devices(pa_context *ctx, const pa_server_info *server_info, void *userdata) { + (void)ctx; + pa_handle *handle = (pa_handle*)userdata; + pthread_mutex_lock(&handle->reconnect_mutex); + + if(server_info->default_sink_name) { + // TODO: Size check + snprintf(handle->default_output_device_name, sizeof(handle->default_output_device_name), "%s.monitor", server_info->default_sink_name); + if(handle->device_type == DEVICE_TYPE_DEFAULT_OUTPUT && strcmp(handle->device_name, handle->default_output_device_name) != 0) { + handle->reconnect = true; + handle->reconnect_last_tried_seconds = clock_get_monotonic_seconds(); + // TODO: Size check + snprintf(handle->device_name, sizeof(handle->device_name), "%s", handle->default_output_device_name); + } + } + + if(server_info->default_source_name) { + // TODO: Size check + snprintf(handle->default_input_device_name, sizeof(handle->default_input_device_name), "%s", server_info->default_source_name); + if(handle->device_type == DEVICE_TYPE_DEFAULT_INPUT && strcmp(handle->device_name, handle->default_input_device_name) != 0) { + handle->reconnect = true; + handle->reconnect_last_tried_seconds = clock_get_monotonic_seconds(); + // TODO: Size check + snprintf(handle->device_name, sizeof(handle->device_name), "%s", handle->default_input_device_name); + } + } + + pthread_mutex_unlock(&handle->reconnect_mutex); +} + +static void subscribe_cb(pa_context *c, pa_subscription_event_type_t t, uint32_t idx, void *userdata) { + (void)idx; + pa_handle *handle = (pa_handle*)userdata; + if((t & PA_SUBSCRIPTION_EVENT_FACILITY_MASK) == PA_SUBSCRIPTION_EVENT_SERVER) { + pa_operation *pa = pa_context_get_server_info(c, subscribe_update_default_devices, handle); + if(pa) + pa_operation_unref(pa); + } +} + +static void store_default_devices(pa_context *ctx, const pa_server_info *server_info, void *userdata) { + (void)ctx; + pa_handle *handle = (pa_handle*)userdata; + if(server_info->default_sink_name) + snprintf(handle->default_output_device_name, sizeof(handle->default_output_device_name), "%s.monitor", server_info->default_sink_name); + if(server_info->default_source_name) + snprintf(handle->default_input_device_name, sizeof(handle->default_input_device_name), "%s", server_info->default_source_name); +} + +static bool startup_get_default_devices(pa_handle *p, const char *device_name) { + pa_operation *pa = pa_context_get_server_info(p->context, store_default_devices, p); + while(pa) { + pa_operation_state_t state = pa_operation_get_state(pa); + if(state == PA_OPERATION_DONE) { + pa_operation_unref(pa); + break; + } else if(state == PA_OPERATION_CANCELLED) { + pa_operation_unref(pa); + return false; + } + pa_mainloop_iterate(p->mainloop, 1, NULL); + } + + if(p->default_output_device_name[0] == '\0') { + gsr_log(GSR_LOG_LEVEL_ERROR, "failed to find default audio output device"); + return false; + } + + if(strcmp(device_name, "default_output") == 0) { + snprintf(p->device_name, sizeof(p->device_name), "%s", p->default_output_device_name); + p->device_type = DEVICE_TYPE_DEFAULT_OUTPUT; + } else if(strcmp(device_name, "default_input") == 0) { + snprintf(p->device_name, sizeof(p->device_name), "%s", p->default_input_device_name); + p->device_type = DEVICE_TYPE_DEFAULT_INPUT; + } else { + snprintf(p->device_name, sizeof(p->device_name), "%s", device_name); + p->device_type = DEVICE_TYPE_STANDARD; + } + + return true; +} + +static pa_handle* pa_sound_device_new(const char *server, + const char *name, + const char *device_name, + const char *stream_name, + const pa_sample_spec *ss, + const pa_buffer_attr *attr, + int *rerror) { + pa_handle *p; + int error = PA_ERR_INTERNAL; + pa_operation *pa = NULL; + + p = pa_xnew0(pa_handle, 1); + p->attr = *attr; + p->ss = *ss; + snprintf(p->node_name, sizeof(p->node_name), "%s", name); + snprintf(p->stream_name, sizeof(p->stream_name), "%s", stream_name); + + if(pthread_mutex_init(&p->reconnect_mutex, NULL) != 0) { + gsr_log(GSR_LOG_LEVEL_ERROR, "failed to initialize reconnect mutex"); + *rerror = -1; + pa_xfree(p); + return NULL; + } + p->reconnect_mutex_initialized = true; + + p->reconnect = true; + p->reconnect_last_tried_seconds = clock_get_monotonic_seconds() - (RECONNECT_TRY_TIMEOUT_SECONDS * 1000.0 * 2.0); + p->default_output_device_name[0] = '\0'; + p->default_input_device_name[0] = '\0'; + p->device_type = DEVICE_TYPE_STANDARD; + + const int buffer_size = attr->fragsize; + void *buffer = malloc(buffer_size); + if(!buffer) { + gsr_log(GSR_LOG_LEVEL_ERROR, "failed to allocate buffer for audio"); + *rerror = -1; + pa_sound_device_free(p); + return NULL; + } + + p->output_data = (uint8_t*)buffer; + p->output_length = buffer_size; + p->output_index = 0; + + p->proplist = pa_proplist_new(); + pa_proplist_sets(p->proplist, PA_PROP_MEDIA_ROLE, "production"); + if(strcmp(device_name, "") == 0) { + pa_proplist_sets(p->proplist, "node.autoconnect", "false"); + pa_proplist_sets(p->proplist, "node.dont-reconnect", "true"); + } + + if (!(p->mainloop = pa_mainloop_new())) + goto fail; + + if (!(p->context = pa_context_new_with_proplist(pa_mainloop_get_api(p->mainloop), p->node_name, p->proplist))) + goto fail; + + if (pa_context_connect(p->context, server, PA_CONTEXT_NOFLAGS, NULL) < 0) { + error = pa_context_errno(p->context); + goto fail; + } + + for (;;) { + pa_context_state_t state = pa_context_get_state(p->context); + + if (state == PA_CONTEXT_READY) + break; + + if (!PA_CONTEXT_IS_GOOD(state)) { + error = pa_context_errno(p->context); + goto fail; + } + + pa_mainloop_iterate(p->mainloop, 1, NULL); + } + + if(!startup_get_default_devices(p, device_name)) + goto fail; + + pa_context_set_subscribe_callback(p->context, subscribe_cb, p); + pa = pa_context_subscribe(p->context, PA_SUBSCRIPTION_MASK_SERVER, NULL, NULL); + if(pa) + pa_operation_unref(pa); + + p->connected = true; + return p; + +fail: + if (rerror) + *rerror = error; + pa_sound_device_free(p); + return NULL; +} + +static void pa_sound_device_update_context_status(pa_handle *p) { + if(p->connected || !p->context || pa_context_get_state(p->context) != PA_CONTEXT_READY) + return; + + p->connected = true; + pa_context_set_subscribe_callback(p->context, subscribe_cb, p); + pa_operation *pa = pa_context_subscribe(p->context, PA_SUBSCRIPTION_MASK_SERVER, NULL, NULL); + if(pa) + pa_operation_unref(pa); +} + +static bool pa_sound_device_handle_context_recreate(pa_handle *p) { + if(p->context) { + pa_context_disconnect(p->context); + pa_context_unref(p->context); + p->context = NULL; + p->connected = false; + } + + if (!(p->context = pa_context_new_with_proplist(pa_mainloop_get_api(p->mainloop), p->node_name, p->proplist))) { + gsr_log(GSR_LOG_LEVEL_ERROR, "pa_context_new_with_proplist failed"); + goto fail; + } + + if(pa_context_connect(p->context, NULL, PA_CONTEXT_NOFLAGS, NULL) < 0) { + gsr_log(GSR_LOG_LEVEL_ERROR, "pa_context_connect failed"); + goto fail; + } + + pa_mainloop_iterate(p->mainloop, 0, NULL); + pa_sound_device_update_context_status(p); + return true; + + fail: + if(p->context) { + pa_context_disconnect(p->context); + pa_context_unref(p->context); + p->context = NULL; + } + return false; +} + +static bool pa_sound_device_should_reconnect(pa_handle *p, double now, char *device_name, size_t device_name_size) { + bool should_reconnect = false; + pthread_mutex_lock(&p->reconnect_mutex); + + if(!p->reconnect && (!p->stream || !PA_STREAM_IS_GOOD(pa_stream_get_state(p->stream)))) { + p->reconnect = true; + p->reconnect_last_tried_seconds = now; + } + + if(p->reconnect && now - p->reconnect_last_tried_seconds >= RECONNECT_TRY_TIMEOUT_SECONDS) { + p->reconnect_last_tried_seconds = now; + // TODO: Size check + snprintf(device_name, device_name_size, "%s", p->device_name); + should_reconnect = true; + } + + pthread_mutex_unlock(&p->reconnect_mutex); + return should_reconnect; +} + +static bool pa_sound_device_handle_reconnect(pa_handle *p, char *device_name, size_t device_name_size, double now) { + if(!pa_sound_device_should_reconnect(p, now, device_name, device_name_size)) + return true; + + if(p->stream) { + pa_stream_disconnect(p->stream); + pa_stream_unref(p->stream); + p->stream = NULL; + + pa_sound_device_handle_context_recreate(p); + if(!p->connected) + return false; + } + + pa_proplist *proplist = pa_proplist_new(); + // This prevents microphone recording indicator from being shown on KDE + pa_proplist_sets(proplist, "node.virtual", "true"); + + if(!(p->stream = pa_stream_new_with_proplist(p->context, p->stream_name, &p->ss, NULL, proplist))) { + //pa_context_errno(p->context); + pa_proplist_free(proplist); + return false; + } + + pa_proplist_free(proplist); + + const int r = pa_stream_connect_record(p->stream, device_name, &p->attr, + (pa_stream_flags_t)(PA_STREAM_INTERPOLATE_TIMING|PA_STREAM_ADJUST_LATENCY|PA_STREAM_AUTO_TIMING_UPDATE|PA_STREAM_DONT_MOVE)); + + if(r < 0) { + //pa_context_errno(p->context); + return false; + } + + pa_mainloop_iterate(p->mainloop, 0, NULL); + + pthread_mutex_lock(&p->reconnect_mutex); + p->reconnect = false; + pthread_mutex_unlock(&p->reconnect_mutex); + return true; +} + +static int pa_sound_device_read(pa_handle *p, double timeout_seconds) { + assert(p); + + const double start_time = clock_get_monotonic_seconds(); + char device_name[DEVICE_NAME_MAX_SIZE]; + + bool success = false; + int r = 0; + int *rerror = &r; + pa_usec_t latency = 0; + int negative = 0; + + pa_mainloop_iterate(p->mainloop, 0, NULL); + + if(!p->context) { + if(!pa_sound_device_handle_context_recreate(p)) + goto fail; + } + + pa_sound_device_update_context_status(p); + if(!p->connected) + goto fail; + + if(!pa_sound_device_handle_reconnect(p, device_name, sizeof(device_name), start_time) || !p->stream) + goto fail; + + if(pa_stream_get_state(p->stream) != PA_STREAM_READY) + goto fail; + + CHECK_DEAD_GOTO(p, rerror, fail); + + while (p->output_index < p->output_length) { + if(clock_get_monotonic_seconds() - start_time >= timeout_seconds) + return -1; + + if(!p->read_data) { + pa_mainloop_prepare(p->mainloop, 1 * 1000); // 1 ms + pa_mainloop_poll(p->mainloop); + pa_mainloop_dispatch(p->mainloop); + + if(pa_stream_peek(p->stream, &p->read_data, &p->read_length) < 0) + goto fail; + + if(!p->read_data && p->read_length == 0) + continue; + + if(!p->read_data && p->read_length > 0) { + // There is a hole in the stream :( drop it. Maybe we should generate silence instead? TODO + if(pa_stream_drop(p->stream) != 0) + goto fail; + continue; + } + + if(p->read_length <= 0) { + p->read_data = NULL; + if(pa_stream_drop(p->stream) != 0) + goto fail; + + CHECK_DEAD_GOTO(p, rerror, fail); + continue; + } + + pa_operation_unref(pa_stream_update_timing_info(p->stream, NULL, NULL)); + // TODO: Deal with one pa_stream_peek not being enough. In that case we need to add multiple of these together(?) + if(pa_stream_get_latency(p->stream, &latency, &negative) >= 0) { + p->latency_seconds = negative ? -(double)latency : latency; + if(p->latency_seconds < 0.0) + p->latency_seconds = 0.0; + p->latency_seconds *= 0.0000001; + } + } + + const size_t space_free_in_output_buffer = p->output_length - p->output_index; + if(space_free_in_output_buffer < p->read_length) { + memcpy(p->output_data + p->output_index, (const uint8_t*)p->read_data + p->read_index, space_free_in_output_buffer); + p->output_index = 0; + p->read_index += space_free_in_output_buffer; + p->read_length -= space_free_in_output_buffer; + break; + } else { + memcpy(p->output_data + p->output_index, (const uint8_t*)p->read_data + p->read_index, p->read_length); + p->output_index += p->read_length; + p->read_data = NULL; + p->read_length = 0; + p->read_index = 0; + + if(pa_stream_drop(p->stream) != 0) + goto fail; + + if(p->output_index == p->output_length) { + p->output_index = 0; + break; + } + } + } + + success = true; + + fail: + return success ? 0 : -1; +} + +static pa_sample_format_t audio_format_to_pulse_audio_format(gsr_audio_format audio_format) { + switch(audio_format) { + case GSR_AUDIO_FORMAT_S16: return PA_SAMPLE_S16LE; + case GSR_AUDIO_FORMAT_S32: return PA_SAMPLE_S32LE; + case GSR_AUDIO_FORMAT_F32: return PA_SAMPLE_FLOAT32LE; + } + assert(false); + return PA_SAMPLE_S16LE; +} + +static int audio_format_to_get_bytes_per_sample(gsr_audio_format audio_format) { + switch(audio_format) { + case GSR_AUDIO_FORMAT_S16: return 2; + case GSR_AUDIO_FORMAT_S32: return 4; + case GSR_AUDIO_FORMAT_F32: return 4; + } + assert(false); + return 2; +} + +int sound_device_get_by_name(SoundDevice *device, const char *node_name, const char *device_name, const char *description, unsigned int num_channels, unsigned int period_frame_size, gsr_audio_format audio_format) { + pa_sample_spec ss; + ss.format = audio_format_to_pulse_audio_format(audio_format); + ss.rate = 48000; + ss.channels = num_channels; + + pa_buffer_attr buffer_attr; + buffer_attr.fragsize = period_frame_size * audio_format_to_get_bytes_per_sample(audio_format) * num_channels; // 2/4 bytes/sample, @num_channels channels + buffer_attr.tlength = -1; + buffer_attr.prebuf = -1; + buffer_attr.minreq = -1; + buffer_attr.maxlength = buffer_attr.fragsize; + + int error = 0; + pa_handle *handle = pa_sound_device_new(NULL, node_name, device_name, description, &ss, &buffer_attr, &error); + if(!handle) { + gsr_log(GSR_LOG_LEVEL_ERROR, "pa_sound_device_new() failed: %s. Audio input device %s might not be valid", pa_strerror(error), device_name); + return -1; + } + + device->handle = handle; + device->frames = period_frame_size; + return 0; +} + +void sound_device_close(SoundDevice *device) { + if(device->handle) + pa_sound_device_free((pa_handle*)device->handle); + device->handle = NULL; +} + +void sound_device_flush(SoundDevice *device) { + pa_handle *p = (pa_handle*)device->handle; + if(!p || !p->stream) + return; + + if(pa_stream_get_state(p->stream) != PA_STREAM_READY) + return; + + pa_operation *op = pa_stream_flush(p->stream, NULL, NULL); + if(!op) + return; + + /* The flush operation should finish immediately, the timeout is to not freeze if pulseaudio is in a bad state */ + const double start_time = clock_get_monotonic_seconds(); + while(pa_operation_get_state(op) == PA_OPERATION_RUNNING && clock_get_monotonic_seconds() - start_time < 0.1) { + pa_mainloop_iterate(p->mainloop, 1, NULL); + } + pa_operation_unref(op); +} + +int sound_device_read_next_chunk(SoundDevice *device, void **buffer, double timeout_sec, double *latency_seconds) { + pa_handle *pa = (pa_handle*)device->handle; + if(pa_sound_device_read(pa, timeout_sec) < 0) { + //gsr_log(GSR_LOG_LEVEL_ERROR, "pa_simple_read() failed: %s", pa_strerror(error)); + *latency_seconds = 0.0; + return -1; + } + *buffer = pa->output_data; + *latency_seconds = pa->latency_seconds; + return device->frames; +} + +static void pa_state_cb(pa_context *c, void *userdata) { + pa_context_state_t state = pa_context_get_state(c); + int *pa_ready = (int*)userdata; + switch(state) { + case PA_CONTEXT_UNCONNECTED: + case PA_CONTEXT_CONNECTING: + case PA_CONTEXT_AUTHORIZING: + case PA_CONTEXT_SETTING_NAME: + default: + break; + case PA_CONTEXT_FAILED: + case PA_CONTEXT_TERMINATED: + *pa_ready = 2; + break; + case PA_CONTEXT_READY: + *pa_ready = 1; + break; + } +} + +static void pa_sourcelist_cb(pa_context *ctx, const pa_source_info *source_info, int eol, void *userdata) { + (void)ctx; + if(eol > 0) + return; + + gsr_audio_devices *audio_devices = (gsr_audio_devices*)userdata; + if(!gsr_array_ensure_capacity((void**)&audio_devices->items, audio_devices->num_items, &audio_devices->capacity_items, sizeof(gsr_audio_device))) + return; + + gsr_audio_device *audio_device = &audio_devices->items[audio_devices->num_items]; + snprintf(audio_device->name, sizeof(audio_device->name), "%s", source_info->name); + snprintf(audio_device->description, sizeof(audio_device->description), "%s", source_info->description); + ++audio_devices->num_items; +} + +static void pa_server_info_cb(pa_context *ctx, const pa_server_info *server_info, void *userdata) { + (void)ctx; + gsr_audio_devices *audio_devices = (gsr_audio_devices*)userdata; + if(server_info->default_sink_name) + snprintf(audio_devices->default_output, sizeof(audio_devices->default_output), "%s.monitor", server_info->default_sink_name); + if(server_info->default_source_name) + snprintf(audio_devices->default_input, sizeof(audio_devices->default_input), "%s", server_info->default_source_name); +} + +static void server_info_callback(pa_context *ctx, const pa_server_info *server_info, void *userdata) { + (void)ctx; + bool *is_server_pipewire = (bool*)userdata; + if(server_info->server_name && strstr(server_info->server_name, "PipeWire")) + *is_server_pipewire = true; +} + +static void get_pulseaudio_default_inputs(gsr_audio_devices *audio_devices) { + int state = 0; + int pa_ready = 0; + pa_operation *pa_op = NULL; + + pa_mainloop *main_loop = pa_mainloop_new(); + if(!main_loop) + return; + + pa_context *ctx = pa_context_new(pa_mainloop_get_api(main_loop), "gpu-screen-recorder"); + if(pa_context_connect(ctx, NULL, PA_CONTEXT_NOFLAGS, NULL) < 0) + goto done; + + pa_context_set_state_callback(ctx, pa_state_cb, &pa_ready); + + for(;;) { + // Not ready + if(pa_ready == 0) { + pa_mainloop_iterate(main_loop, 1, NULL); + continue; + } + + switch(state) { + case 0: { + pa_op = pa_context_get_server_info(ctx, pa_server_info_cb, audio_devices); + ++state; + break; + } + } + + // Couldn't get connection to the server + if(pa_ready == 2 || (state == 1 && pa_op && pa_operation_get_state(pa_op) == PA_OPERATION_DONE)) + break; + + pa_mainloop_iterate(main_loop, 1, NULL); + } + + done: + if(pa_op) + pa_operation_unref(pa_op); + pa_context_disconnect(ctx); + pa_context_unref(ctx); + pa_mainloop_free(main_loop); +} + +void get_pulseaudio_inputs(gsr_audio_devices *audio_devices) { + memset(audio_devices, 0, sizeof(*audio_devices)); + + int state = 0; + int pa_ready = 0; + pa_operation *pa_op = NULL; + + // TODO: Do this in the same connection below instead of two separate connections + get_pulseaudio_default_inputs(audio_devices); + + pa_mainloop *main_loop = pa_mainloop_new(); + if(!main_loop) + return; + + pa_context *ctx = pa_context_new(pa_mainloop_get_api(main_loop), "gpu-screen-recorder"); + if(pa_context_connect(ctx, NULL, PA_CONTEXT_NOFLAGS, NULL) < 0) + goto done; + + pa_context_set_state_callback(ctx, pa_state_cb, &pa_ready); + + for(;;) { + // Not ready + if(pa_ready == 0) { + pa_mainloop_iterate(main_loop, 1, NULL); + continue; + } + + switch(state) { + case 0: { + pa_op = pa_context_get_source_info_list(ctx, pa_sourcelist_cb, audio_devices); + ++state; + break; + } + } + + // Couldn't get connection to the server + if(pa_ready == 2 || (state == 1 && pa_op && pa_operation_get_state(pa_op) == PA_OPERATION_DONE)) + break; + + pa_mainloop_iterate(main_loop, 1, NULL); + } + + done: + if(pa_op) + pa_operation_unref(pa_op); + pa_context_disconnect(ctx); + pa_context_unref(ctx); + pa_mainloop_free(main_loop); +} + +void gsr_audio_devices_deinit(gsr_audio_devices *self) { + if(self->items) { + free(self->items); + self->items = NULL; + } + self->num_items = 0; + self->capacity_items = 0; + self->default_output[0] = '\0'; + self->default_input[0] = '\0'; +} + +bool pulseaudio_server_is_pipewire(void) { + int state = 0; + int pa_ready = 0; + pa_operation *pa_op = NULL; + bool is_server_pipewire = false; + + pa_mainloop *main_loop = pa_mainloop_new(); + if(!main_loop) + return is_server_pipewire; + + pa_context *ctx = pa_context_new(pa_mainloop_get_api(main_loop), "gpu-screen-recorder"); + if(pa_context_connect(ctx, NULL, PA_CONTEXT_NOFLAGS, NULL) < 0) + goto done; + + pa_context_set_state_callback(ctx, pa_state_cb, &pa_ready); + + for(;;) { + // Not ready + if(pa_ready == 0) { + pa_mainloop_iterate(main_loop, 1, NULL); + continue; + } + + switch(state) { + case 0: { + pa_op = pa_context_get_server_info(ctx, server_info_callback, &is_server_pipewire); + ++state; + break; + } + } + + // Couldn't get connection to the server + if(pa_ready == 2 || (state == 1 && pa_op && pa_operation_get_state(pa_op) == PA_OPERATION_DONE)) + break; + + pa_mainloop_iterate(main_loop, 1, NULL); + } + + done: + if(pa_op) + pa_operation_unref(pa_op); + pa_context_disconnect(ctx); + pa_context_unref(ctx); + pa_mainloop_free(main_loop); + return is_server_pipewire; +} diff --git a/src/sound.cpp b/src/sound.cpp deleted file mode 100644 index 9a72e78..0000000 --- a/src/sound.cpp +++ /dev/null @@ -1,738 +0,0 @@ -#include "../include/sound.hpp" -#include "../include/log.h" -extern "C" { -#include "../include/utils.h" -} - -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include - -#define RECONNECT_TRY_TIMEOUT_SECONDS 0.5 -#define DEVICE_NAME_MAX_SIZE 128 - -#define CHECK_DEAD_GOTO(p, rerror, label) \ - do { \ - if (!(p)->context || !PA_CONTEXT_IS_GOOD(pa_context_get_state((p)->context)) || \ - !(p)->stream || !PA_STREAM_IS_GOOD(pa_stream_get_state((p)->stream))) { \ - if (((p)->context && pa_context_get_state((p)->context) == PA_CONTEXT_FAILED) || \ - ((p)->stream && pa_stream_get_state((p)->stream) == PA_STREAM_FAILED)) { \ - if (rerror) \ - *(rerror) = pa_context_errno((p)->context); \ - } else \ - if (rerror) \ - *(rerror) = PA_ERR_BADSTATE; \ - goto label; \ - } \ - } while(false); - -enum class DeviceType { - STANDARD, - DEFAULT_OUTPUT, - DEFAULT_INPUT -}; - -struct pa_handle { - pa_context *context; - pa_stream *stream; - pa_mainloop *mainloop; - - const void *read_data; - size_t read_index, read_length; - - uint8_t *output_data; - size_t output_index, output_length; - - int operation_success; - double latency_seconds; - - pa_buffer_attr attr; - pa_sample_spec ss; - - std::mutex reconnect_mutex; - DeviceType device_type; - char stream_name[256]; - char node_name[256]; - bool reconnect; - double reconnect_last_tried_seconds; - - char device_name[DEVICE_NAME_MAX_SIZE]; - char default_output_device_name[DEVICE_NAME_MAX_SIZE]; - char default_input_device_name[DEVICE_NAME_MAX_SIZE]; - - pa_proplist *proplist; - bool connected; -}; - -static void pa_sound_device_free(pa_handle *p) { - assert(p); - - if(p->proplist) { - pa_proplist_free(p->proplist); - p->proplist = NULL; - } - - if (p->stream) { - pa_stream_unref(p->stream); - p->stream = NULL; - } - - if (p->context) { - pa_context_disconnect(p->context); - pa_context_unref(p->context); - p->context = NULL; - } - - if (p->mainloop) { - pa_mainloop_free(p->mainloop); - p->mainloop = NULL; - } - - if (p->output_data) { - free(p->output_data); - p->output_data = NULL; - } - - pa_xfree(p); -} - -static void subscribe_update_default_devices(pa_context*, const pa_server_info *server_info, void *userdata) { - pa_handle *handle = (pa_handle*)userdata; - std::lock_guard lock(handle->reconnect_mutex); - - if(server_info->default_sink_name) { - // TODO: Size check - snprintf(handle->default_output_device_name, sizeof(handle->default_output_device_name), "%s.monitor", server_info->default_sink_name); - if(handle->device_type == DeviceType::DEFAULT_OUTPUT && strcmp(handle->device_name, handle->default_output_device_name) != 0) { - handle->reconnect = true; - handle->reconnect_last_tried_seconds = clock_get_monotonic_seconds(); - // TODO: Size check - snprintf(handle->device_name, sizeof(handle->device_name), "%s", handle->default_output_device_name); - } - } - - if(server_info->default_source_name) { - // TODO: Size check - snprintf(handle->default_input_device_name, sizeof(handle->default_input_device_name), "%s", server_info->default_source_name); - if(handle->device_type == DeviceType::DEFAULT_INPUT && strcmp(handle->device_name, handle->default_input_device_name) != 0) { - handle->reconnect = true; - handle->reconnect_last_tried_seconds = clock_get_monotonic_seconds(); - // TODO: Size check - snprintf(handle->device_name, sizeof(handle->device_name), "%s", handle->default_input_device_name); - } - } -} - -static void subscribe_cb(pa_context *c, pa_subscription_event_type_t t, uint32_t idx, void *userdata) { - (void)idx; - pa_handle *handle = (pa_handle*)userdata; - if((t & PA_SUBSCRIPTION_EVENT_FACILITY_MASK) == PA_SUBSCRIPTION_EVENT_SERVER) { - pa_operation *pa = pa_context_get_server_info(c, subscribe_update_default_devices, handle); - if(pa) - pa_operation_unref(pa); - } -} - -static void store_default_devices(pa_context*, const pa_server_info *server_info, void *userdata) { - pa_handle *handle = (pa_handle*)userdata; - if(server_info->default_sink_name) - snprintf(handle->default_output_device_name, sizeof(handle->default_output_device_name), "%s.monitor", server_info->default_sink_name); - if(server_info->default_source_name) - snprintf(handle->default_input_device_name, sizeof(handle->default_input_device_name), "%s", server_info->default_source_name); -} - -static bool startup_get_default_devices(pa_handle *p, const char *device_name) { - pa_operation *pa = pa_context_get_server_info(p->context, store_default_devices, p); - while(pa) { - pa_operation_state state = pa_operation_get_state(pa); - if(state == PA_OPERATION_DONE) { - pa_operation_unref(pa); - break; - } else if(state == PA_OPERATION_CANCELLED) { - pa_operation_unref(pa); - return false; - } - pa_mainloop_iterate(p->mainloop, 1, NULL); - } - - if(p->default_output_device_name[0] == '\0') { - gsr_log(GSR_LOG_LEVEL_ERROR, "failed to find default audio output device"); - return false; - } - - if(strcmp(device_name, "default_output") == 0) { - snprintf(p->device_name, sizeof(p->device_name), "%s", p->default_output_device_name); - p->device_type = DeviceType::DEFAULT_OUTPUT; - } else if(strcmp(device_name, "default_input") == 0) { - snprintf(p->device_name, sizeof(p->device_name), "%s", p->default_input_device_name); - p->device_type = DeviceType::DEFAULT_INPUT; - } else { - snprintf(p->device_name, sizeof(p->device_name), "%s", device_name); - p->device_type = DeviceType::STANDARD; - } - - return true; -} - -static pa_handle* pa_sound_device_new(const char *server, - const char *name, - const char *device_name, - const char *stream_name, - const pa_sample_spec *ss, - const pa_buffer_attr *attr, - int *rerror) { - pa_handle *p; - int error = PA_ERR_INTERNAL; - pa_operation *pa = NULL; - - p = pa_xnew0(pa_handle, 1); - p->attr = *attr; - p->ss = *ss; - snprintf(p->node_name, sizeof(p->node_name), "%s", name); - snprintf(p->stream_name, sizeof(p->stream_name), "%s", stream_name); - - p->reconnect = true; - p->reconnect_last_tried_seconds = clock_get_monotonic_seconds() - (RECONNECT_TRY_TIMEOUT_SECONDS * 1000.0 * 2.0); - p->default_output_device_name[0] = '\0'; - p->default_input_device_name[0] = '\0'; - p->device_type = DeviceType::STANDARD; - - const int buffer_size = attr->fragsize; - void *buffer = malloc(buffer_size); - if(!buffer) { - gsr_log(GSR_LOG_LEVEL_ERROR, "failed to allocate buffer for audio"); - *rerror = -1; - return NULL; - } - - p->output_data = (uint8_t*)buffer; - p->output_length = buffer_size; - p->output_index = 0; - - p->proplist = pa_proplist_new(); - pa_proplist_sets(p->proplist, PA_PROP_MEDIA_ROLE, "production"); - if(strcmp(device_name, "") == 0) { - pa_proplist_sets(p->proplist, "node.autoconnect", "false"); - pa_proplist_sets(p->proplist, "node.dont-reconnect", "true"); - } - - if (!(p->mainloop = pa_mainloop_new())) - goto fail; - - if (!(p->context = pa_context_new_with_proplist(pa_mainloop_get_api(p->mainloop), p->node_name, p->proplist))) - goto fail; - - if (pa_context_connect(p->context, server, PA_CONTEXT_NOFLAGS, NULL) < 0) { - error = pa_context_errno(p->context); - goto fail; - } - - for (;;) { - pa_context_state_t state = pa_context_get_state(p->context); - - if (state == PA_CONTEXT_READY) - break; - - if (!PA_CONTEXT_IS_GOOD(state)) { - error = pa_context_errno(p->context); - goto fail; - } - - pa_mainloop_iterate(p->mainloop, 1, NULL); - } - - if(!startup_get_default_devices(p, device_name)) - goto fail; - - pa_context_set_subscribe_callback(p->context, subscribe_cb, p); - pa = pa_context_subscribe(p->context, PA_SUBSCRIPTION_MASK_SERVER, NULL, NULL); - if(pa) - pa_operation_unref(pa); - - p->connected = true; - return p; - -fail: - if (rerror) - *rerror = error; - pa_sound_device_free(p); - return NULL; -} - -static void pa_sound_device_update_context_status(pa_handle *p) { - if(p->connected || !p->context || pa_context_get_state(p->context) != PA_CONTEXT_READY) - return; - - p->connected = true; - pa_context_set_subscribe_callback(p->context, subscribe_cb, p); - pa_operation *pa = pa_context_subscribe(p->context, PA_SUBSCRIPTION_MASK_SERVER, NULL, NULL); - if(pa) - pa_operation_unref(pa); -} - -static bool pa_sound_device_handle_context_recreate(pa_handle *p) { - if(p->context) { - pa_context_disconnect(p->context); - pa_context_unref(p->context); - p->context = NULL; - p->connected = false; - } - - if (!(p->context = pa_context_new_with_proplist(pa_mainloop_get_api(p->mainloop), p->node_name, p->proplist))) { - gsr_log(GSR_LOG_LEVEL_ERROR, "pa_context_new_with_proplist failed"); - goto fail; - } - - if(pa_context_connect(p->context, nullptr, PA_CONTEXT_NOFLAGS, NULL) < 0) { - gsr_log(GSR_LOG_LEVEL_ERROR, "pa_context_connect failed"); - goto fail; - } - - pa_mainloop_iterate(p->mainloop, 0, NULL); - pa_sound_device_update_context_status(p); - return true; - - fail: - if(p->context) { - pa_context_disconnect(p->context); - pa_context_unref(p->context); - p->context = NULL; - } - return false; -} - -static bool pa_sound_device_should_reconnect(pa_handle *p, double now, char *device_name, size_t device_name_size) { - std::lock_guard lock(p->reconnect_mutex); - - if(!p->reconnect && (!p->stream || !PA_STREAM_IS_GOOD(pa_stream_get_state(p->stream)))) { - p->reconnect = true; - p->reconnect_last_tried_seconds = now; - } - - if(p->reconnect && now - p->reconnect_last_tried_seconds >= RECONNECT_TRY_TIMEOUT_SECONDS) { - p->reconnect_last_tried_seconds = now; - // TODO: Size check - snprintf(device_name, device_name_size, "%s", p->device_name); - return true; - } - - return false; -} - -static bool pa_sound_device_handle_reconnect(pa_handle *p, char *device_name, size_t device_name_size, double now) { - if(!pa_sound_device_should_reconnect(p, now, device_name, device_name_size)) - return true; - - if(p->stream) { - pa_stream_disconnect(p->stream); - pa_stream_unref(p->stream); - p->stream = NULL; - - pa_sound_device_handle_context_recreate(p); - if(!p->connected) - return false; - } - - pa_proplist *proplist = pa_proplist_new(); - // This prevents microphone recording indicator from being shown on KDE - pa_proplist_sets(proplist, "node.virtual", "true"); - - if(!(p->stream = pa_stream_new_with_proplist(p->context, p->stream_name, &p->ss, NULL, proplist))) { - //pa_context_errno(p->context); - pa_proplist_free(proplist); - return false; - } - - pa_proplist_free(proplist); - - const int r = pa_stream_connect_record(p->stream, device_name, &p->attr, - (pa_stream_flags_t)(PA_STREAM_INTERPOLATE_TIMING|PA_STREAM_ADJUST_LATENCY|PA_STREAM_AUTO_TIMING_UPDATE|PA_STREAM_DONT_MOVE)); - - if(r < 0) { - //pa_context_errno(p->context); - return false; - } - - pa_mainloop_iterate(p->mainloop, 0, NULL); - - std::lock_guard lock(p->reconnect_mutex); - p->reconnect = false; - return true; -} - -static int pa_sound_device_read(pa_handle *p, double timeout_seconds) { - assert(p); - - const double start_time = clock_get_monotonic_seconds(); - char device_name[DEVICE_NAME_MAX_SIZE]; - - bool success = false; - int r = 0; - int *rerror = &r; - pa_usec_t latency = 0; - int negative = 0; - - pa_mainloop_iterate(p->mainloop, 0, NULL); - - if(!p->context) { - if(!pa_sound_device_handle_context_recreate(p)) - goto fail; - } - - pa_sound_device_update_context_status(p); - if(!p->connected) - goto fail; - - if(!pa_sound_device_handle_reconnect(p, device_name, sizeof(device_name), start_time) || !p->stream) - goto fail; - - if(pa_stream_get_state(p->stream) != PA_STREAM_READY) - goto fail; - - CHECK_DEAD_GOTO(p, rerror, fail); - - while (p->output_index < p->output_length) { - if(clock_get_monotonic_seconds() - start_time >= timeout_seconds) - return -1; - - if(!p->read_data) { - pa_mainloop_prepare(p->mainloop, 1 * 1000); // 1 ms - pa_mainloop_poll(p->mainloop); - pa_mainloop_dispatch(p->mainloop); - - if(pa_stream_peek(p->stream, &p->read_data, &p->read_length) < 0) - goto fail; - - if(!p->read_data && p->read_length == 0) - continue; - - if(!p->read_data && p->read_length > 0) { - // There is a hole in the stream :( drop it. Maybe we should generate silence instead? TODO - if(pa_stream_drop(p->stream) != 0) - goto fail; - continue; - } - - if(p->read_length <= 0) { - p->read_data = NULL; - if(pa_stream_drop(p->stream) != 0) - goto fail; - - CHECK_DEAD_GOTO(p, rerror, fail); - continue; - } - - pa_operation_unref(pa_stream_update_timing_info(p->stream, NULL, NULL)); - // TODO: Deal with one pa_stream_peek not being enough. In that case we need to add multiple of these together(?) - if(pa_stream_get_latency(p->stream, &latency, &negative) >= 0) { - p->latency_seconds = negative ? -(double)latency : latency; - if(p->latency_seconds < 0.0) - p->latency_seconds = 0.0; - p->latency_seconds *= 0.0000001; - } - } - - const size_t space_free_in_output_buffer = p->output_length - p->output_index; - if(space_free_in_output_buffer < p->read_length) { - memcpy(p->output_data + p->output_index, (const uint8_t*)p->read_data + p->read_index, space_free_in_output_buffer); - p->output_index = 0; - p->read_index += space_free_in_output_buffer; - p->read_length -= space_free_in_output_buffer; - break; - } else { - memcpy(p->output_data + p->output_index, (const uint8_t*)p->read_data + p->read_index, p->read_length); - p->output_index += p->read_length; - p->read_data = NULL; - p->read_length = 0; - p->read_index = 0; - - if(pa_stream_drop(p->stream) != 0) - goto fail; - - if(p->output_index == p->output_length) { - p->output_index = 0; - break; - } - } - } - - success = true; - - fail: - return success ? 0 : -1; -} - -static pa_sample_format_t audio_format_to_pulse_audio_format(AudioFormat audio_format) { - switch(audio_format) { - case S16: return PA_SAMPLE_S16LE; - case S32: return PA_SAMPLE_S32LE; - case F32: return PA_SAMPLE_FLOAT32LE; - } - assert(false); - return PA_SAMPLE_S16LE; -} - -static int audio_format_to_get_bytes_per_sample(AudioFormat audio_format) { - switch(audio_format) { - case S16: return 2; - case S32: return 4; - case F32: return 4; - } - assert(false); - return 2; -} - -int sound_device_get_by_name(SoundDevice *device, const char *node_name, const char *device_name, const char *description, unsigned int num_channels, unsigned int period_frame_size, AudioFormat audio_format) { - pa_sample_spec ss; - ss.format = audio_format_to_pulse_audio_format(audio_format); - ss.rate = 48000; - ss.channels = num_channels; - - pa_buffer_attr buffer_attr; - buffer_attr.fragsize = period_frame_size * audio_format_to_get_bytes_per_sample(audio_format) * num_channels; // 2/4 bytes/sample, @num_channels channels - buffer_attr.tlength = -1; - buffer_attr.prebuf = -1; - buffer_attr.minreq = -1; - buffer_attr.maxlength = buffer_attr.fragsize; - - int error = 0; - pa_handle *handle = pa_sound_device_new(nullptr, node_name, device_name, description, &ss, &buffer_attr, &error); - if(!handle) { - gsr_log(GSR_LOG_LEVEL_ERROR, "pa_sound_device_new() failed: %s. Audio input device %s might not be valid", pa_strerror(error), device_name); - return -1; - } - - device->handle = handle; - device->frames = period_frame_size; - return 0; -} - -void sound_device_close(SoundDevice *device) { - if(device->handle) - pa_sound_device_free((pa_handle*)device->handle); - device->handle = NULL; -} - -void sound_device_flush(SoundDevice *device) { - pa_handle *p = (pa_handle*)device->handle; - if(!p || !p->stream) - return; - - if(pa_stream_get_state(p->stream) != PA_STREAM_READY) - return; - - pa_operation *op = pa_stream_flush(p->stream, NULL, NULL); - if(!op) - return; - - /* The flush operation should finish immediately, the timeout is to not freeze if pulseaudio is in a bad state */ - const double start_time = clock_get_monotonic_seconds(); - while(pa_operation_get_state(op) == PA_OPERATION_RUNNING && clock_get_monotonic_seconds() - start_time < 0.1) { - pa_mainloop_iterate(p->mainloop, 1, NULL); - } - pa_operation_unref(op); -} - -int sound_device_read_next_chunk(SoundDevice *device, void **buffer, double timeout_sec, double *latency_seconds) { - pa_handle *pa = (pa_handle*)device->handle; - if(pa_sound_device_read(pa, timeout_sec) < 0) { - //fprintf(stderr, "pa_simple_read() failed: %s\n", pa_strerror(error)); - *latency_seconds = 0.0; - return -1; - } - *buffer = pa->output_data; - *latency_seconds = pa->latency_seconds; - return device->frames; -} - -static void pa_state_cb(pa_context *c, void *userdata) { - pa_context_state state = pa_context_get_state(c); - int *pa_ready = (int*)userdata; - switch(state) { - case PA_CONTEXT_UNCONNECTED: - case PA_CONTEXT_CONNECTING: - case PA_CONTEXT_AUTHORIZING: - case PA_CONTEXT_SETTING_NAME: - default: - break; - case PA_CONTEXT_FAILED: - case PA_CONTEXT_TERMINATED: - *pa_ready = 2; - break; - case PA_CONTEXT_READY: - *pa_ready = 1; - break; - } -} - -static void pa_sourcelist_cb(pa_context*, const pa_source_info *source_info, int eol, void *userdata) { - if(eol > 0) - return; - - AudioDevices *audio_devices = (AudioDevices*)userdata; - audio_devices->audio_inputs.push_back({ source_info->name, source_info->description }); -} - -static void pa_server_info_cb(pa_context*, const pa_server_info *server_info, void *userdata) { - AudioDevices *audio_devices = (AudioDevices*)userdata; - if(server_info->default_sink_name) - audio_devices->default_output = std::string(server_info->default_sink_name) + ".monitor"; - if(server_info->default_source_name) - audio_devices->default_input = server_info->default_source_name; -} - -static void server_info_callback(pa_context*, const pa_server_info *server_info, void *userdata) { - bool *is_server_pipewire = (bool*)userdata; - if(server_info->server_name && strstr(server_info->server_name, "PipeWire")) - *is_server_pipewire = true; -} - -static void get_pulseaudio_default_inputs(AudioDevices &audio_devices) { - int state = 0; - int pa_ready = 0; - pa_operation *pa_op = NULL; - - pa_mainloop *main_loop = pa_mainloop_new(); - if(!main_loop) - return; - - pa_context *ctx = pa_context_new(pa_mainloop_get_api(main_loop), "gpu-screen-recorder"); - if(pa_context_connect(ctx, NULL, PA_CONTEXT_NOFLAGS, NULL) < 0) - goto done; - - pa_context_set_state_callback(ctx, pa_state_cb, &pa_ready); - - for(;;) { - // Not ready - if(pa_ready == 0) { - pa_mainloop_iterate(main_loop, 1, NULL); - continue; - } - - switch(state) { - case 0: { - pa_op = pa_context_get_server_info(ctx, pa_server_info_cb, &audio_devices); - ++state; - break; - } - } - - // Couldn't get connection to the server - if(pa_ready == 2 || (state == 1 && pa_op && pa_operation_get_state(pa_op) == PA_OPERATION_DONE)) - break; - - pa_mainloop_iterate(main_loop, 1, NULL); - } - - done: - if(pa_op) - pa_operation_unref(pa_op); - pa_context_disconnect(ctx); - pa_context_unref(ctx); - pa_mainloop_free(main_loop); -} - -AudioDevices get_pulseaudio_inputs() { - AudioDevices audio_devices; - int state = 0; - int pa_ready = 0; - pa_operation *pa_op = NULL; - - // TODO: Do this in the same connection below instead of two separate connections - get_pulseaudio_default_inputs(audio_devices); - - pa_mainloop *main_loop = pa_mainloop_new(); - if(!main_loop) - return audio_devices; - - pa_context *ctx = pa_context_new(pa_mainloop_get_api(main_loop), "gpu-screen-recorder"); - if(pa_context_connect(ctx, NULL, PA_CONTEXT_NOFLAGS, NULL) < 0) - goto done; - - pa_context_set_state_callback(ctx, pa_state_cb, &pa_ready); - - for(;;) { - // Not ready - if(pa_ready == 0) { - pa_mainloop_iterate(main_loop, 1, NULL); - continue; - } - - switch(state) { - case 0: { - pa_op = pa_context_get_source_info_list(ctx, pa_sourcelist_cb, &audio_devices); - ++state; - break; - } - } - - // Couldn't get connection to the server - if(pa_ready == 2 || (state == 1 && pa_op && pa_operation_get_state(pa_op) == PA_OPERATION_DONE)) - break; - - pa_mainloop_iterate(main_loop, 1, NULL); - } - - done: - if(pa_op) - pa_operation_unref(pa_op); - pa_context_disconnect(ctx); - pa_context_unref(ctx); - pa_mainloop_free(main_loop); - return audio_devices; -} - -bool pulseaudio_server_is_pipewire() { - int state = 0; - int pa_ready = 0; - pa_operation *pa_op = NULL; - bool is_server_pipewire = false; - - pa_mainloop *main_loop = pa_mainloop_new(); - if(!main_loop) - return is_server_pipewire; - - pa_context *ctx = pa_context_new(pa_mainloop_get_api(main_loop), "gpu-screen-recorder"); - if(pa_context_connect(ctx, NULL, PA_CONTEXT_NOFLAGS, NULL) < 0) - goto done; - - pa_context_set_state_callback(ctx, pa_state_cb, &pa_ready); - - for(;;) { - // Not ready - if(pa_ready == 0) { - pa_mainloop_iterate(main_loop, 1, NULL); - continue; - } - - switch(state) { - case 0: { - pa_op = pa_context_get_server_info(ctx, server_info_callback, &is_server_pipewire); - ++state; - break; - } - } - - // Couldn't get connection to the server - if(pa_ready == 2 || (state == 1 && pa_op && pa_operation_get_state(pa_op) == PA_OPERATION_DONE)) - break; - - pa_mainloop_iterate(main_loop, 1, NULL); - } - - done: - if(pa_op) - pa_operation_unref(pa_op); - pa_context_disconnect(ctx); - pa_context_unref(ctx); - pa_mainloop_free(main_loop); - return is_server_pipewire; -} -- cgit v1.2.3 From e8fca67df16025598f6ca7bcc31cfdcfb79feb7d Mon Sep 17 00:00:00 2001 From: dec05eba Date: Sat, 1 Aug 2026 21:14:46 +0200 Subject: Move audio/video codec setup from main.cpp into C modules Adds gsr_recorder_settings, a library-friendly settings struct embedded in args_parser, so the new modules don't depend on the argument parser. The codec functions return errors instead of exiting. --- include/args_parser.h | 46 +- include/ffmpeg_utils.h | 14 + include/recorder/audio_codec.h | 23 + include/recorder/settings.h | 63 +++ include/recorder/video_codec.h | 18 + meson.build | 3 + src/args_parser.c | 194 ++++----- src/ffmpeg_utils.c | 12 + src/main.cpp | 955 +++++++---------------------------------- src/recorder/audio_codec.c | 220 ++++++++++ src/recorder/video_codec.c | 429 ++++++++++++++++++ 11 files changed, 1047 insertions(+), 930 deletions(-) create mode 100644 include/ffmpeg_utils.h create mode 100644 include/recorder/audio_codec.h create mode 100644 include/recorder/settings.h create mode 100644 include/recorder/video_codec.h create mode 100644 src/ffmpeg_utils.c create mode 100644 src/recorder/audio_codec.c create mode 100644 src/recorder/video_codec.c diff --git a/include/args_parser.h b/include/args_parser.h index e8c68d8..9e388ec 100644 --- a/include/args_parser.h +++ b/include/args_parser.h @@ -5,6 +5,7 @@ #include #include "defs.h" #include "vec2.h" +#include "recorder/settings.h" typedef struct gsr_egl gsr_egl; @@ -68,50 +69,7 @@ typedef struct { typedef struct { Arg args[NUM_ARGS]; - - gsr_video_encoder_hardware video_encoder; - gsr_pixel_format pixel_format; - gsr_framerate_mode framerate_mode; - gsr_color_range color_range; - gsr_tune tune; - gsr_video_codec video_codec; - gsr_audio_codec audio_codec; - gsr_bitrate_mode bitrate_mode; - gsr_video_quality video_quality; - gsr_replay_storage replay_storage; - - const char *capture_source; - const char *container_format; - const char *filename; - const char *replay_recording_directory; - const char *portal_session_token_filepath; - const char *recording_saved_script; - const char *ffmpeg_opts; - const char *ffmpeg_video_opts; - const char *ffmpeg_audio_opts; - bool verbose; - bool gl_debug; - bool fallback_cpu_encoding; - bool low_power; - bool exclude_metadata; - bool record_cursor; - bool date_folders; - bool restore_portal_session; - bool restart_replay_on_save; - bool write_first_frame_ts; - bool is_replaying; - bool is_livestream; - bool is_output_piped; - bool low_latency_recording; - bool very_old_gpu; - int64_t video_bitrate; - int64_t audio_bitrate; - int64_t fps; - int64_t replay_buffer_size_secs; - double keyint; - vec2i output_resolution; - vec2i region_size; - vec2i region_position; + gsr_recorder_settings settings; } args_parser; /* |argv| is stored as a reference */ diff --git a/include/ffmpeg_utils.h b/include/ffmpeg_utils.h new file mode 100644 index 0000000..373122c --- /dev/null +++ b/include/ffmpeg_utils.h @@ -0,0 +1,14 @@ +#ifndef GSR_FFMPEG_UTILS_H +#define GSR_FFMPEG_UTILS_H + +#ifdef __cplusplus +extern "C" { +#endif + +const char* gsr_av_error_to_string(int err); + +#ifdef __cplusplus +} +#endif + +#endif /* GSR_FFMPEG_UTILS_H */ diff --git a/include/recorder/audio_codec.h b/include/recorder/audio_codec.h new file mode 100644 index 0000000..d432abd --- /dev/null +++ b/include/recorder/audio_codec.h @@ -0,0 +1,23 @@ +#ifndef GSR_RECORDER_AUDIO_CODEC_H +#define GSR_RECORDER_AUDIO_CODEC_H + +#include +#include +#include "../defs.h" +#include "../sound.h" + +#include + +#define GSR_AUDIO_SAMPLE_RATE 48000 + +gsr_audio_format audio_codec_context_get_audio_format(const AVCodecContext *audio_codec_context); +enum AVSampleFormat audio_format_to_sample_format(const gsr_audio_format audio_format); + +AVCodecContext* create_audio_codec_context(int fps, gsr_audio_codec audio_codec, bool mix_audio, int64_t audio_bitrate); +bool open_audio(AVCodecContext *audio_codec_context, const char *ffmpeg_audio_opts); +AVFrame* create_audio_frame(AVCodecContext *audio_codec_context); + +double audio_codec_get_desired_delay(gsr_audio_codec audio_codec, int fps); +int audio_codec_get_frame_size(gsr_audio_codec audio_codec); + +#endif /* GSR_RECORDER_AUDIO_CODEC_H */ diff --git a/include/recorder/settings.h b/include/recorder/settings.h new file mode 100644 index 0000000..8bb7f77 --- /dev/null +++ b/include/recorder/settings.h @@ -0,0 +1,63 @@ +#ifndef GSR_RECORDER_SETTINGS_H +#define GSR_RECORDER_SETTINGS_H + +#include +#include +#include "../defs.h" +#include "../vec2.h" + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct { + gsr_video_encoder_hardware video_encoder; + gsr_pixel_format pixel_format; + gsr_framerate_mode framerate_mode; + gsr_color_range color_range; + gsr_tune tune; + gsr_video_codec video_codec; + gsr_audio_codec audio_codec; + gsr_bitrate_mode bitrate_mode; + gsr_video_quality video_quality; + gsr_replay_storage replay_storage; + + const char *capture_source; + const char *container_format; + const char *filename; + const char *replay_recording_directory; + const char *portal_session_token_filepath; + const char *recording_saved_script; + const char *ffmpeg_opts; + const char *ffmpeg_video_opts; + const char *ffmpeg_audio_opts; + bool verbose; + bool gl_debug; + bool fallback_cpu_encoding; + bool low_power; + bool exclude_metadata; + bool record_cursor; + bool date_folders; + bool restore_portal_session; + bool restart_replay_on_save; + bool write_first_frame_ts; + bool is_replaying; + bool is_livestream; + bool is_output_piped; + bool low_latency_recording; + bool very_old_gpu; + int64_t video_bitrate; + int64_t audio_bitrate; + int64_t fps; + int64_t replay_buffer_size_secs; + double keyint; + vec2i output_resolution; + vec2i region_size; + vec2i region_position; +} gsr_recorder_settings; + +#ifdef __cplusplus +} +#endif + +#endif /* GSR_RECORDER_SETTINGS_H */ diff --git a/include/recorder/video_codec.h b/include/recorder/video_codec.h new file mode 100644 index 0000000..9347a9e --- /dev/null +++ b/include/recorder/video_codec.h @@ -0,0 +1,18 @@ +#ifndef GSR_RECORDER_VIDEO_CODEC_H +#define GSR_RECORDER_VIDEO_CODEC_H + +#include +#include "../defs.h" +#include "../egl.h" +#include "settings.h" + +#include + +int video_quality_to_h264_equivalent_qp(gsr_video_quality video_quality); +enum AVPixelFormat get_pixel_format(gsr_video_codec video_codec, gsr_gpu_vendor vendor, bool use_software_video_encoder); + +AVCodecContext* create_video_codec_context(enum AVPixelFormat pix_fmt, const AVCodec *codec, const gsr_egl *egl, const gsr_recorder_settings *settings, int width, int height); +bool open_video_software(AVCodecContext *codec_context, const gsr_recorder_settings *settings); +bool open_video_hardware(AVCodecContext *codec_context, bool low_power, const gsr_egl *egl, const gsr_recorder_settings *settings); + +#endif /* GSR_RECORDER_VIDEO_CODEC_H */ diff --git a/meson.build b/meson.build index ebe8440..220ecbf 100644 --- a/meson.build +++ b/meson.build @@ -31,6 +31,9 @@ src = [ 'src/replay_buffer/replay_buffer_ram.c', 'src/replay_buffer/replay_buffer_disk.c', 'src/log.c', + 'src/ffmpeg_utils.c', + 'src/recorder/audio_codec.c', + 'src/recorder/video_codec.c', 'src/egl.c', 'src/cuda.c', 'src/window_texture.c', diff --git a/src/args_parser.c b/src/args_parser.c index 039dfca..b105f84 100644 --- a/src/args_parser.c +++ b/src/args_parser.c @@ -253,104 +253,104 @@ static bool file_is_pipe_or_char_device(const char *filepath) { } static bool args_parser_set_values(args_parser *self) { - self->video_encoder = (gsr_video_encoder_hardware)args_get_enum_by_key(self->args, NUM_ARGS, "-encoder", GSR_VIDEO_ENCODER_HW_GPU); - self->pixel_format = (gsr_pixel_format)args_get_enum_by_key(self->args, NUM_ARGS, "-pixfmt", GSR_PIXEL_FORMAT_YUV420); - self->framerate_mode = (gsr_framerate_mode)args_get_enum_by_key(self->args, NUM_ARGS, "-fm", GSR_FRAMERATE_MODE_VARIABLE); - self->color_range = (gsr_color_range)args_get_enum_by_key(self->args, NUM_ARGS, "-cr", GSR_COLOR_RANGE_LIMITED); - self->tune = (gsr_tune)args_get_enum_by_key(self->args, NUM_ARGS, "-tune", GSR_TUNE_PERFORMANCE); - self->video_codec = (gsr_video_codec)args_get_enum_by_key(self->args, NUM_ARGS, "-k", GSR_VIDEO_CODEC_AUTO); - self->audio_codec = (gsr_audio_codec)args_get_enum_by_key(self->args, NUM_ARGS, "-ac", GSR_AUDIO_CODEC_OPUS); - self->bitrate_mode = (gsr_bitrate_mode)args_get_enum_by_key(self->args, NUM_ARGS, "-bm", GSR_BITRATE_MODE_AUTO); - self->replay_storage = (gsr_replay_storage)args_get_enum_by_key(self->args, NUM_ARGS, "-replay-storage", GSR_REPLAY_STORAGE_RAM); - - self->capture_source = args_get_value_by_key(self->args, NUM_ARGS, "-w"); - self->verbose = args_get_boolean_by_key(self->args, NUM_ARGS, "-v", true); - self->gl_debug = args_get_boolean_by_key(self->args, NUM_ARGS, "-gl-debug", false); - self->record_cursor = args_get_boolean_by_key(self->args, NUM_ARGS, "-cursor", true); - self->date_folders = args_get_boolean_by_key(self->args, NUM_ARGS, "-df", false); - self->restore_portal_session = args_get_boolean_by_key(self->args, NUM_ARGS, "-restore-portal-session", false); - self->restart_replay_on_save = args_get_boolean_by_key(self->args, NUM_ARGS, "-restart-replay-on-save", false); + self->settings.video_encoder = (gsr_video_encoder_hardware)args_get_enum_by_key(self->args, NUM_ARGS, "-encoder", GSR_VIDEO_ENCODER_HW_GPU); + self->settings.pixel_format = (gsr_pixel_format)args_get_enum_by_key(self->args, NUM_ARGS, "-pixfmt", GSR_PIXEL_FORMAT_YUV420); + self->settings.framerate_mode = (gsr_framerate_mode)args_get_enum_by_key(self->args, NUM_ARGS, "-fm", GSR_FRAMERATE_MODE_VARIABLE); + self->settings.color_range = (gsr_color_range)args_get_enum_by_key(self->args, NUM_ARGS, "-cr", GSR_COLOR_RANGE_LIMITED); + self->settings.tune = (gsr_tune)args_get_enum_by_key(self->args, NUM_ARGS, "-tune", GSR_TUNE_PERFORMANCE); + self->settings.video_codec = (gsr_video_codec)args_get_enum_by_key(self->args, NUM_ARGS, "-k", GSR_VIDEO_CODEC_AUTO); + self->settings.audio_codec = (gsr_audio_codec)args_get_enum_by_key(self->args, NUM_ARGS, "-ac", GSR_AUDIO_CODEC_OPUS); + self->settings.bitrate_mode = (gsr_bitrate_mode)args_get_enum_by_key(self->args, NUM_ARGS, "-bm", GSR_BITRATE_MODE_AUTO); + self->settings.replay_storage = (gsr_replay_storage)args_get_enum_by_key(self->args, NUM_ARGS, "-replay-storage", GSR_REPLAY_STORAGE_RAM); + + self->settings.capture_source = args_get_value_by_key(self->args, NUM_ARGS, "-w"); + self->settings.verbose = args_get_boolean_by_key(self->args, NUM_ARGS, "-v", true); + self->settings.gl_debug = args_get_boolean_by_key(self->args, NUM_ARGS, "-gl-debug", false); + self->settings.record_cursor = args_get_boolean_by_key(self->args, NUM_ARGS, "-cursor", true); + self->settings.date_folders = args_get_boolean_by_key(self->args, NUM_ARGS, "-df", false); + self->settings.restore_portal_session = args_get_boolean_by_key(self->args, NUM_ARGS, "-restore-portal-session", false); + self->settings.restart_replay_on_save = args_get_boolean_by_key(self->args, NUM_ARGS, "-restart-replay-on-save", false); const bool overclock = args_get_boolean_by_key(self->args, NUM_ARGS, "-oc", false); - self->fallback_cpu_encoding = args_get_boolean_by_key(self->args, NUM_ARGS, "-fallback-cpu-encoding", false); - self->write_first_frame_ts = args_get_boolean_by_key(self->args, NUM_ARGS, "-write-first-frame-ts", false); - self->low_power = args_get_boolean_by_key(self->args, NUM_ARGS, "-low-power", false); - self->exclude_metadata = args_get_boolean_by_key(self->args, NUM_ARGS, "-exclude-metadata", false); + self->settings.fallback_cpu_encoding = args_get_boolean_by_key(self->args, NUM_ARGS, "-fallback-cpu-encoding", false); + self->settings.write_first_frame_ts = args_get_boolean_by_key(self->args, NUM_ARGS, "-write-first-frame-ts", false); + self->settings.low_power = args_get_boolean_by_key(self->args, NUM_ARGS, "-low-power", false); + self->settings.exclude_metadata = args_get_boolean_by_key(self->args, NUM_ARGS, "-exclude-metadata", false); - self->audio_bitrate = args_get_i64_by_key(self->args, NUM_ARGS, "-ab", 0); - self->audio_bitrate *= 1000LL; + self->settings.audio_bitrate = args_get_i64_by_key(self->args, NUM_ARGS, "-ab", 0); + self->settings.audio_bitrate *= 1000LL; - self->keyint = args_get_double_by_key(self->args, NUM_ARGS, "-keyint", 2.0); + self->settings.keyint = args_get_double_by_key(self->args, NUM_ARGS, "-keyint", 2.0); if(overclock) { gsr_log(GSR_LOG_LEVEL_INFO, "the overclock option (-oc) is deprecated and no longer has any effect as it's no longer needed (on GPUs that are 12 years old or younger)"); } - if(self->audio_codec == GSR_AUDIO_CODEC_FLAC) { + if(self->settings.audio_codec == GSR_AUDIO_CODEC_FLAC) { gsr_log(GSR_LOG_LEVEL_WARNING, "flac audio codec is temporary disabled, using opus audio codec instead"); - self->audio_codec = GSR_AUDIO_CODEC_OPUS; + self->settings.audio_codec = GSR_AUDIO_CODEC_OPUS; } - self->portal_session_token_filepath = args_get_value_by_key(self->args, NUM_ARGS, "-portal-session-token-filepath"); - if(self->portal_session_token_filepath) { - int len = strlen(self->portal_session_token_filepath); - if(len > 0 && self->portal_session_token_filepath[len - 1] == '/') { - gsr_log(GSR_LOG_LEVEL_ERROR, "-portal-session-token-filepath should be a path to a file but it ends with a /: %s", self->portal_session_token_filepath); + self->settings.portal_session_token_filepath = args_get_value_by_key(self->args, NUM_ARGS, "-portal-session-token-filepath"); + if(self->settings.portal_session_token_filepath) { + int len = strlen(self->settings.portal_session_token_filepath); + if(len > 0 && self->settings.portal_session_token_filepath[len - 1] == '/') { + gsr_log(GSR_LOG_LEVEL_ERROR, "-portal-session-token-filepath should be a path to a file but it ends with a /: %s", self->settings.portal_session_token_filepath); return false; } } - self->recording_saved_script = args_get_value_by_key(self->args, NUM_ARGS, "-sc"); - if(self->recording_saved_script) { + self->settings.recording_saved_script = args_get_value_by_key(self->args, NUM_ARGS, "-sc"); + if(self->settings.recording_saved_script) { struct stat buf; - if(stat(self->recording_saved_script, &buf) == -1 || !S_ISREG(buf.st_mode)) { - gsr_log(GSR_LOG_LEVEL_ERROR, "Script \"%s\" either doesn't exist or it's not a file", self->recording_saved_script); + if(stat(self->settings.recording_saved_script, &buf) == -1 || !S_ISREG(buf.st_mode)) { + gsr_log(GSR_LOG_LEVEL_ERROR, "Script \"%s\" either doesn't exist or it's not a file", self->settings.recording_saved_script); usage(); return false; } if(!(buf.st_mode & S_IXUSR)) { - gsr_log(GSR_LOG_LEVEL_ERROR, "Script \"%s\" is not executable", self->recording_saved_script); + gsr_log(GSR_LOG_LEVEL_ERROR, "Script \"%s\" is not executable", self->settings.recording_saved_script); usage(); return false; } } const char *quality_str = args_get_value_by_key(self->args, NUM_ARGS, "-q"); - self->video_quality = GSR_VIDEO_QUALITY_VERY_HIGH; - self->video_bitrate = 0; + self->settings.video_quality = GSR_VIDEO_QUALITY_VERY_HIGH; + self->settings.video_bitrate = 0; - if(self->bitrate_mode == GSR_BITRATE_MODE_CBR) { + if(self->settings.bitrate_mode == GSR_BITRATE_MODE_CBR) { if(!quality_str) { gsr_log(GSR_LOG_LEVEL_ERROR, "option '-q' is required when using '-bm cbr' option"); usage(); return false; } - if(sscanf(quality_str, "%" PRIi64, &self->video_bitrate) != 1) { + if(sscanf(quality_str, "%" PRIi64, &self->settings.video_bitrate) != 1) { gsr_log(GSR_LOG_LEVEL_ERROR, "-q argument \"%s\" is not an integer value. When using '-bm cbr' option '-q' is expected to be an integer value", quality_str); usage(); return false; } - if(self->video_bitrate < 0) { - gsr_log(GSR_LOG_LEVEL_ERROR, "-q is expected to be 0 or larger, got %" PRIi64, self->video_bitrate); + if(self->settings.video_bitrate < 0) { + gsr_log(GSR_LOG_LEVEL_ERROR, "-q is expected to be 0 or larger, got %" PRIi64, self->settings.video_bitrate); usage(); return false; } - self->video_bitrate *= 1000LL; + self->settings.video_bitrate *= 1000LL; } else { if(!quality_str) quality_str = "very_high"; if(strcmp(quality_str, "medium") == 0) { - self->video_quality = GSR_VIDEO_QUALITY_MEDIUM; + self->settings.video_quality = GSR_VIDEO_QUALITY_MEDIUM; } else if(strcmp(quality_str, "high") == 0) { - self->video_quality = GSR_VIDEO_QUALITY_HIGH; + self->settings.video_quality = GSR_VIDEO_QUALITY_HIGH; } else if(strcmp(quality_str, "very_high") == 0) { - self->video_quality = GSR_VIDEO_QUALITY_VERY_HIGH; + self->settings.video_quality = GSR_VIDEO_QUALITY_VERY_HIGH; } else if(strcmp(quality_str, "ultra") == 0) { - self->video_quality = GSR_VIDEO_QUALITY_ULTRA; + self->settings.video_quality = GSR_VIDEO_QUALITY_ULTRA; } else { gsr_log(GSR_LOG_LEVEL_ERROR, "-q should either be 'medium', 'high', 'very_high' or 'ultra', got: '%s'", quality_str); usage(); @@ -358,118 +358,118 @@ static bool args_parser_set_values(args_parser *self) { } } - self->output_resolution = (vec2i){0, 0}; + self->settings.output_resolution = (vec2i){0, 0}; const char *output_resolution_str = args_get_value_by_key(self->args, NUM_ARGS, "-s"); if(output_resolution_str) { - if(sscanf(output_resolution_str, "%dx%d", &self->output_resolution.x, &self->output_resolution.y) != 2) { + if(sscanf(output_resolution_str, "%dx%d", &self->settings.output_resolution.x, &self->settings.output_resolution.y) != 2) { gsr_log(GSR_LOG_LEVEL_ERROR, "invalid value for option -s '%s', expected a value in format WxH", output_resolution_str); usage(); return false; } - if(self->output_resolution.x < 0 || self->output_resolution.y < 0) { + if(self->settings.output_resolution.x < 0 || self->settings.output_resolution.y < 0) { gsr_log(GSR_LOG_LEVEL_ERROR, "invalid value for option -s '%s', expected width and height to be greater or equal to 0", output_resolution_str); usage(); return false; } } - self->region_size = (vec2i){0, 0}; - self->region_position = (vec2i){0, 0}; + self->settings.region_size = (vec2i){0, 0}; + self->settings.region_position = (vec2i){0, 0}; const char *region_str = args_get_value_by_key(self->args, NUM_ARGS, "-region"); if(region_str) { - if(sscanf(region_str, "%dx%d+%d+%d", &self->region_size.x, &self->region_size.y, &self->region_position.x, &self->region_position.y) != 4) { + if(sscanf(region_str, "%dx%d+%d+%d", &self->settings.region_size.x, &self->settings.region_size.y, &self->settings.region_position.x, &self->settings.region_position.y) != 4) { gsr_log(GSR_LOG_LEVEL_ERROR, "invalid value for option -region '%s', expected a value in format WxH+X+Y", region_str); usage(); return false; } - if(self->region_size.x < 0 || self->region_size.y < 0) { + if(self->settings.region_size.x < 0 || self->settings.region_size.y < 0) { gsr_log(GSR_LOG_LEVEL_ERROR, "invalid value for option -region '%s', expected width and height to be greater or equal to 0", region_str); usage(); return false; } } - self->fps = args_get_i64_by_key(self->args, NUM_ARGS, "-f", 60); - self->replay_buffer_size_secs = args_get_i64_by_key(self->args, NUM_ARGS, "-r", -1); - if(self->replay_buffer_size_secs != -1) - self->replay_buffer_size_secs += (int64_t)(self->keyint + 0.5); // Add a few seconds to account of lost packets because of non-keyframe packets skipped + self->settings.fps = args_get_i64_by_key(self->args, NUM_ARGS, "-f", 60); + self->settings.replay_buffer_size_secs = args_get_i64_by_key(self->args, NUM_ARGS, "-r", -1); + if(self->settings.replay_buffer_size_secs != -1) + self->settings.replay_buffer_size_secs += (int64_t)(self->settings.keyint + 0.5); // Add a few seconds to account of lost packets because of non-keyframe packets skipped - self->container_format = args_get_value_by_key(self->args, NUM_ARGS, "-c"); - if(self->container_format && strcmp(self->container_format, "mkv") == 0) - self->container_format = "matroska"; + self->settings.container_format = args_get_value_by_key(self->args, NUM_ARGS, "-c"); + if(self->settings.container_format && strcmp(self->settings.container_format, "mkv") == 0) + self->settings.container_format = "matroska"; - self->is_replaying = self->replay_buffer_size_secs != -1; - self->is_livestream = false; - self->filename = args_get_value_by_key(self->args, NUM_ARGS, "-o"); - if(self->filename) { - self->is_livestream = is_livestream_path(self->filename); - if(self->is_livestream) { - if(self->is_replaying) { + self->settings.is_replaying = self->settings.replay_buffer_size_secs != -1; + self->settings.is_livestream = false; + self->settings.filename = args_get_value_by_key(self->args, NUM_ARGS, "-o"); + if(self->settings.filename) { + self->settings.is_livestream = is_livestream_path(self->settings.filename); + if(self->settings.is_livestream) { + if(self->settings.is_replaying) { gsr_log(GSR_LOG_LEVEL_ERROR, "replay mode is not applicable to live streaming"); return false; } } else { - if(!self->is_replaying) { + if(!self->settings.is_replaying) { char directory_buf[PATH_MAX]; - snprintf(directory_buf, sizeof(directory_buf), "%s", self->filename); + snprintf(directory_buf, sizeof(directory_buf), "%s", self->settings.filename); char *directory = dirname(directory_buf); if(strcmp(directory, ".") != 0 && strcmp(directory, "/") != 0) { if(create_directory_recursive(directory) != 0) { - gsr_log(GSR_LOG_LEVEL_ERROR, "failed to create directory for output file: %s", self->filename); + gsr_log(GSR_LOG_LEVEL_ERROR, "failed to create directory for output file: %s", self->settings.filename); return false; } } } else { - if(!self->container_format) { + if(!self->settings.container_format) { gsr_log(GSR_LOG_LEVEL_ERROR, "option -c is required when using option -r"); usage(); return false; } struct stat buf; - if(stat(self->filename, &buf) != -1 && !S_ISDIR(buf.st_mode)) { - gsr_log(GSR_LOG_LEVEL_ERROR, "File \"%s\" exists but it's not a directory", self->filename); + if(stat(self->settings.filename, &buf) != -1 && !S_ISDIR(buf.st_mode)) { + gsr_log(GSR_LOG_LEVEL_ERROR, "File \"%s\" exists but it's not a directory", self->settings.filename); usage(); return false; } } } } else { - if(!self->is_replaying) { - self->filename = "/dev/stdout"; + if(!self->settings.is_replaying) { + self->settings.filename = "/dev/stdout"; } else { gsr_log(GSR_LOG_LEVEL_ERROR, "Option -o is required when using option -r"); usage(); return false; } - if(!self->container_format) { + if(!self->settings.container_format) { gsr_log(GSR_LOG_LEVEL_ERROR, "option -c is required when not using option -o"); usage(); return false; } } - self->is_output_piped = file_is_pipe_or_char_device(self->filename); - self->low_latency_recording = self->is_livestream || self->is_output_piped; - if(self->write_first_frame_ts && (self->is_livestream || self->is_output_piped)) { + self->settings.is_output_piped = file_is_pipe_or_char_device(self->settings.filename); + self->settings.low_latency_recording = self->settings.is_livestream || self->settings.is_output_piped; + if(self->settings.write_first_frame_ts && (self->settings.is_livestream || self->settings.is_output_piped)) { gsr_log(GSR_LOG_LEVEL_WARNING, "-write-first-frame-ts is ignored for livestreaming or when output is piped"); - self->write_first_frame_ts = false; + self->settings.write_first_frame_ts = false; } - self->replay_recording_directory = args_get_value_by_key(self->args, NUM_ARGS, "-ro"); + self->settings.replay_recording_directory = args_get_value_by_key(self->args, NUM_ARGS, "-ro"); - if(self->is_livestream && self->recording_saved_script) { + if(self->settings.is_livestream && self->settings.recording_saved_script) { gsr_log(GSR_LOG_LEVEL_WARNING, "live stream detected, -sc script is ignored"); - self->recording_saved_script = NULL; + self->settings.recording_saved_script = NULL; } - self->ffmpeg_opts = args_get_value_by_key(self->args, NUM_ARGS, "-ffmpeg-opts"); - self->ffmpeg_video_opts = args_get_value_by_key(self->args, NUM_ARGS, "-ffmpeg-video-opts"); - self->ffmpeg_audio_opts = args_get_value_by_key(self->args, NUM_ARGS, "-ffmpeg-audio-opts"); + self->settings.ffmpeg_opts = args_get_value_by_key(self->args, NUM_ARGS, "-ffmpeg-opts"); + self->settings.ffmpeg_video_opts = args_get_value_by_key(self->args, NUM_ARGS, "-ffmpeg-video-opts"); + self->settings.ffmpeg_audio_opts = args_get_value_by_key(self->args, NUM_ARGS, "-ffmpeg-audio-opts"); return true; } @@ -690,33 +690,33 @@ void args_parser_deinit(args_parser *self) { bool args_parser_validate_with_gl_info(args_parser *self, gsr_egl *egl) { const bool wayland = gsr_window_get_display_server(egl->window) == GSR_DISPLAY_SERVER_WAYLAND; - if(self->bitrate_mode == (gsr_bitrate_mode)GSR_BITRATE_MODE_AUTO) { + if(self->settings.bitrate_mode == (gsr_bitrate_mode)GSR_BITRATE_MODE_AUTO) { // QP is broken on steam deck, see https://github.com/ValveSoftware/SteamOS/issues/1609 - self->bitrate_mode = egl->gpu_info.is_steam_deck ? GSR_BITRATE_MODE_VBR : GSR_BITRATE_MODE_QP; + self->settings.bitrate_mode = egl->gpu_info.is_steam_deck ? GSR_BITRATE_MODE_VBR : GSR_BITRATE_MODE_QP; } - if(egl->gpu_info.is_steam_deck && self->bitrate_mode == GSR_BITRATE_MODE_QP) { + if(egl->gpu_info.is_steam_deck && self->settings.bitrate_mode == GSR_BITRATE_MODE_QP) { gsr_log(GSR_LOG_LEVEL_WARNING, "qp bitrate mode is not supported on Steam Deck because of Steam Deck driver bugs. Using vbr instead"); - self->bitrate_mode = GSR_BITRATE_MODE_VBR; + self->settings.bitrate_mode = GSR_BITRATE_MODE_VBR; } - if(self->video_encoder == GSR_VIDEO_ENCODER_HW_CPU && self->bitrate_mode == GSR_BITRATE_MODE_VBR) { + if(self->settings.video_encoder == GSR_VIDEO_ENCODER_HW_CPU && self->settings.bitrate_mode == GSR_BITRATE_MODE_VBR) { gsr_log(GSR_LOG_LEVEL_WARNING, "bitrate mode has been forcefully set to qp because software encoding option doesn't support vbr option"); - self->bitrate_mode = GSR_BITRATE_MODE_QP; + self->settings.bitrate_mode = GSR_BITRATE_MODE_QP; } if(egl->gpu_info.is_steam_deck) { gsr_log(GSR_LOG_LEVEL_WARNING, "steam deck has multiple driver issues. One of them has been reported here: https://github.com/ValveSoftware/SteamOS/issues/1609\nIf you have issues with GPU Screen Recorder on steam deck that you don't have on a desktop computer then report the issue to Valve and/or AMD."); } - self->very_old_gpu = false; + self->settings.very_old_gpu = false; if(egl->gpu_info.vendor == GSR_GPU_VENDOR_NVIDIA && egl->gpu_info.gpu_version != 0 && egl->gpu_info.gpu_version < 900) { gsr_log(GSR_LOG_LEVEL_INFO, "your gpu appears to be very old (older than maxwell architecture). Switching to lower preset"); - self->very_old_gpu = true; + self->settings.very_old_gpu = true; } - if(video_codec_is_hdr(self->video_codec) && !wayland) { - gsr_log(GSR_LOG_LEVEL_ERROR, "hdr video codec option %s is not available on X11", video_codec_to_string(self->video_codec)); + if(video_codec_is_hdr(self->settings.video_codec) && !wayland) { + gsr_log(GSR_LOG_LEVEL_ERROR, "hdr video codec option %s is not available on X11", video_codec_to_string(self->settings.video_codec)); usage(); return false; } diff --git a/src/ffmpeg_utils.c b/src/ffmpeg_utils.c new file mode 100644 index 0000000..ce239a1 --- /dev/null +++ b/src/ffmpeg_utils.c @@ -0,0 +1,12 @@ +#include "../include/ffmpeg_utils.h" + +#include +#include + +static _Thread_local char av_error_buffer[AV_ERROR_MAX_STRING_SIZE]; + +const char* gsr_av_error_to_string(int err) { + if(av_strerror(err, av_error_buffer, sizeof(av_error_buffer)) < 0) + strcpy(av_error_buffer, "Unknown error"); + return av_error_buffer; +} diff --git a/src/main.cpp b/src/main.cpp index 6862b99..b618a3b 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,4 +1,7 @@ extern "C" { +#include "../include/ffmpeg_utils.h" +#include "../include/recorder/audio_codec.h" +#include "../include/recorder/video_codec.h" #include "../include/capture/nvfbc.h" #include "../include/capture/xcomposite.h" #include "../include/capture/ximage.h" @@ -91,12 +94,8 @@ struct MergedAudioInputs { // TODO: Remove LIBAVUTIL_VERSION_MAJOR checks in the future when ubuntu, pop os LTS etc update ffmpeg to >= 5.0 -static const int AUDIO_SAMPLE_RATE = 48000; - static const int VIDEO_STREAM_INDEX = 0; -static thread_local char av_error_buffer[AV_ERROR_MAX_STRING_SIZE]; - typedef struct { const gsr_window *window; } MonitorOutputCallbackUserdata; @@ -150,12 +149,6 @@ static void get_monitor_by_position_callback(const gsr_monitor *monitor, void *u } } -static char* av_error_to_string(int err) { - if(av_strerror(err, av_error_buffer, sizeof(av_error_buffer)) < 0) - strcpy(av_error_buffer, "Unknown error"); - return av_error_buffer; -} - static int x11_error_handler(Display*, XErrorEvent*) { return 0; } @@ -164,595 +157,9 @@ static int x11_io_error_handler(Display*) { return 0; } -static AVCodecID audio_codec_get_id(gsr_audio_codec audio_codec) { - switch(audio_codec) { - case GSR_AUDIO_CODEC_AAC: return AV_CODEC_ID_AAC; - case GSR_AUDIO_CODEC_OPUS: return AV_CODEC_ID_OPUS; - case GSR_AUDIO_CODEC_FLAC: return AV_CODEC_ID_FLAC; - } - assert(false); - return AV_CODEC_ID_AAC; -} - -static AVSampleFormat audio_codec_get_sample_format(AVCodecContext *audio_codec_context, gsr_audio_codec audio_codec, const AVCodec *codec, bool mix_audio) { - (void)audio_codec_context; - switch(audio_codec) { - case GSR_AUDIO_CODEC_AAC: { - return AV_SAMPLE_FMT_FLTP; - } - case GSR_AUDIO_CODEC_OPUS: { - bool supports_s16 = false; - bool supports_flt = false; - - #if LIBAVCODEC_VERSION_INT < AV_VERSION_INT(61, 15, 0) - for(size_t i = 0; codec->sample_fmts && codec->sample_fmts[i] != AV_SAMPLE_FMT_NONE; ++i) { - if(codec->sample_fmts[i] == AV_SAMPLE_FMT_S16) { - supports_s16 = true; - } else if(codec->sample_fmts[i] == AV_SAMPLE_FMT_FLT) { - supports_flt = true; - } - } - #else - const enum AVSampleFormat *sample_fmts = NULL; - if(avcodec_get_supported_config(audio_codec_context, codec, AV_CODEC_CONFIG_SAMPLE_FORMAT, 0, (const void**)&sample_fmts, NULL) >= 0) { - if(sample_fmts) { - for(size_t i = 0; sample_fmts[i] != AV_SAMPLE_FMT_NONE; ++i) { - if(sample_fmts[i] == AV_SAMPLE_FMT_S16) { - supports_s16 = true; - } else if(sample_fmts[i] == AV_SAMPLE_FMT_FLT) { - supports_flt = true; - } - } - } else { - // What a dumb API. It returns NULL if all formats are supported - supports_s16 = true; - supports_flt = true; - } - } - #endif - - // Amix only works with float audio - if(mix_audio) - supports_s16 = false; - - if(!supports_s16 && !supports_flt) { - gsr_log(GSR_LOG_LEVEL_WARNING, "opus audio codec is chosen but your ffmpeg version does not support s16/flt sample format and performance might be slightly worse."); - fprintf(stderr, " You can either rebuild ffmpeg with libopus instead of the built-in opus, use the flatpak version of gpu screen recorder or record with aac audio codec instead (-ac aac).\n"); - fprintf(stderr, " Falling back to fltp audio sample format instead.\n"); - } - - if(supports_s16) - return AV_SAMPLE_FMT_S16; - else if(supports_flt) - return AV_SAMPLE_FMT_FLT; - else - return AV_SAMPLE_FMT_FLTP; - } - case GSR_AUDIO_CODEC_FLAC: { - return AV_SAMPLE_FMT_S32; - } - } - assert(false); - return AV_SAMPLE_FMT_FLTP; -} - -static int64_t audio_codec_get_get_bitrate(gsr_audio_codec audio_codec) { - switch(audio_codec) { - case GSR_AUDIO_CODEC_AAC: return 160000; - case GSR_AUDIO_CODEC_OPUS: return 128000; - case GSR_AUDIO_CODEC_FLAC: return 128000; - } - assert(false); - return 128000; -} - -static gsr_audio_format audio_codec_context_get_audio_format(const AVCodecContext *audio_codec_context) { - switch(audio_codec_context->sample_fmt) { - case AV_SAMPLE_FMT_FLT: return GSR_AUDIO_FORMAT_F32; - case AV_SAMPLE_FMT_FLTP: return GSR_AUDIO_FORMAT_S32; - case AV_SAMPLE_FMT_S16: return GSR_AUDIO_FORMAT_S16; - case AV_SAMPLE_FMT_S32: return GSR_AUDIO_FORMAT_S32; - default: return GSR_AUDIO_FORMAT_S16; - } -} - -static AVSampleFormat audio_format_to_sample_format(const gsr_audio_format audio_format) { - switch(audio_format) { - case GSR_AUDIO_FORMAT_S16: return AV_SAMPLE_FMT_S16; - case GSR_AUDIO_FORMAT_S32: return AV_SAMPLE_FMT_S32; - case GSR_AUDIO_FORMAT_F32: return AV_SAMPLE_FMT_FLT; - } - assert(false); - return AV_SAMPLE_FMT_S16; -} - -static AVCodecContext* create_audio_codec_context(int fps, gsr_audio_codec audio_codec, bool mix_audio, int64_t audio_bitrate) { - (void)fps; - const AVCodec *codec = avcodec_find_encoder(audio_codec_get_id(audio_codec)); - if (!codec) { - gsr_log(GSR_LOG_LEVEL_ERROR, "Could not find %s audio encoder", audio_codec_get_name(audio_codec)); - _exit(1); - } - - AVCodecContext *codec_context = avcodec_alloc_context3(codec); - - assert(codec->type == AVMEDIA_TYPE_AUDIO); - codec_context->codec_id = codec->id; - codec_context->sample_fmt = audio_codec_get_sample_format(codec_context, audio_codec, codec, mix_audio); - codec_context->bit_rate = audio_bitrate == 0 ? audio_codec_get_get_bitrate(audio_codec) : audio_bitrate; - codec_context->sample_rate = AUDIO_SAMPLE_RATE; - if(audio_codec == GSR_AUDIO_CODEC_AAC) { -#if LIBAVCODEC_VERSION_MAJOR < 62 - codec_context->profile = FF_PROFILE_AAC_LOW; -#else - codec_context->profile = AV_PROFILE_AAC_LOW; -#endif - } -#if LIBAVCODEC_VERSION_MAJOR < 60 - codec_context->channel_layout = AV_CH_LAYOUT_STEREO; - codec_context->channels = 2; -#else - av_channel_layout_default(&codec_context->ch_layout, 2); -#endif - - codec_context->time_base.num = 1; - codec_context->time_base.den = codec_context->sample_rate; - codec_context->thread_count = 1; - codec_context->flags |= AV_CODEC_FLAG_GLOBAL_HEADER; - - return codec_context; -} - -static int video_quality_to_h264_equivalent_qp(gsr_video_quality video_quality) { - switch(video_quality) { - case GSR_VIDEO_QUALITY_MEDIUM: return 35; - case GSR_VIDEO_QUALITY_HIGH: return 30; - case GSR_VIDEO_QUALITY_VERY_HIGH: return 25; - case GSR_VIDEO_QUALITY_ULTRA: return 22; - } - return 22; -} - /* AV1/VP9 use the 0-255 qindex quantizer scale and VP8 uses 0-127. Unlike h264 qp the quantizer step size is not exponential in qindex, so the mapping to an equivalent h264 qp is not linear in the quantizer range. qindex = h264 qp * 4 matches the qindex = crf * 4 scale used by libvpx/libaom, where crf is roughly equivalent to h264 qp. */ -static int video_quality_to_codec_quality_value(AVCodecID codec_id, gsr_video_quality video_quality) { - const int h264_qp = video_quality_to_h264_equivalent_qp(video_quality); - switch(codec_id) { - case AV_CODEC_ID_H264: - case AV_CODEC_ID_HEVC: - return h264_qp; - case AV_CODEC_ID_AV1: - case AV_CODEC_ID_VP9: - return h264_qp * 4; - case AV_CODEC_ID_VP8: - return h264_qp * 2; - default: - return h264_qp; - } -} - -static int vbr_get_quality_parameter(AVCodecContext *codec_context, gsr_video_quality video_quality, bool hdr) { - // 8 bit / 10 bit = 80% - const float qp_multiply = hdr ? 8.0f/10.0f : 1.0f; - return video_quality_to_codec_quality_value(codec_context->codec_id, video_quality) * qp_multiply; -} - -static AVCodecContext *create_video_codec_context(AVPixelFormat pix_fmt, const AVCodec *codec, const gsr_egl &egl, const args_parser &arg_parser, int width, int height) { - const bool use_software_video_encoder = arg_parser.video_encoder == GSR_VIDEO_ENCODER_HW_CPU; - const bool hdr = video_codec_is_hdr(arg_parser.video_codec); - AVCodecContext *codec_context = avcodec_alloc_context3(codec); - - //double fps_ratio = (double)fps / 30.0; - - assert(codec->type == AVMEDIA_TYPE_VIDEO); - codec_context->codec_id = codec->id; - codec_context->width = width; - codec_context->height = height; - // Timebase: This is the fundamental unit of time (in seconds) in terms - // of which frame timestamps are represented. For fixed-fps content, - // timebase should be 1/framerate and timestamp increments should be - // identical to 1 - codec_context->time_base.num = 1; - codec_context->time_base.den = arg_parser.framerate_mode == GSR_FRAMERATE_MODE_CONSTANT ? arg_parser.fps : AV_TIME_BASE; - codec_context->framerate.num = arg_parser.fps; - codec_context->framerate.den = 1; - codec_context->sample_aspect_ratio.num = 0; - codec_context->sample_aspect_ratio.den = 0; - if(arg_parser.low_latency_recording) { - codec_context->flags |= (AV_CODEC_FLAG_CLOSED_GOP | AV_CODEC_FLAG_LOW_DELAY); - codec_context->flags2 |= AV_CODEC_FLAG2_FAST; - //codec_context->gop_size = std::numeric_limits::max(); - //codec_context->keyint_min = std::numeric_limits::max(); - codec_context->gop_size = arg_parser.fps * arg_parser.keyint; - } else { - // High values reduce file size but increases time it takes to seek - codec_context->gop_size = arg_parser.fps * arg_parser.keyint; - } - codec_context->max_b_frames = 0; - codec_context->pix_fmt = pix_fmt; - codec_context->color_range = arg_parser.color_range == GSR_COLOR_RANGE_LIMITED ? AVCOL_RANGE_MPEG : AVCOL_RANGE_JPEG; - if(hdr) { - codec_context->color_primaries = AVCOL_PRI_BT2020; - codec_context->color_trc = AVCOL_TRC_SMPTE2084; - codec_context->colorspace = AVCOL_SPC_BT2020_NCL; - } else { - codec_context->color_primaries = AVCOL_PRI_BT709; - codec_context->color_trc = AVCOL_TRC_BT709; - codec_context->colorspace = AVCOL_SPC_BT709; - } - //codec_context->chroma_sample_location = AVCHROMA_LOC_CENTER; - // Can't use this because it's fucking broken in ffmpeg 8 or new mesa. It produces garbage output - //if(codec->id == AV_CODEC_ID_HEVC) - // codec_context->codec_tag = MKTAG('h', 'v', 'c', '1'); // QuickTime on MacOS requires this or the video wont be playable - - if(arg_parser.bitrate_mode == GSR_BITRATE_MODE_CBR) { - codec_context->bit_rate = arg_parser.video_bitrate; - codec_context->rc_max_rate = codec_context->bit_rate; - //codec_context->rc_min_rate = codec_context->bit_rate; - codec_context->rc_buffer_size = codec_context->bit_rate;//codec_context->bit_rate / 10; - codec_context->rc_initial_buffer_occupancy = 0;//codec_context->bit_rate;//codec_context->bit_rate * 1000; - } else if(arg_parser.bitrate_mode == GSR_BITRATE_MODE_VBR) { - const int quality = vbr_get_quality_parameter(codec_context, arg_parser.video_quality, hdr); - switch(arg_parser.video_quality) { - case GSR_VIDEO_QUALITY_MEDIUM: - codec_context->qmin = quality; - codec_context->qmax = quality; - codec_context->bit_rate = 100000;//4500000 + (codec_context->width * codec_context->height)*0.75; - break; - case GSR_VIDEO_QUALITY_HIGH: - codec_context->qmin = quality; - codec_context->qmax = quality; - codec_context->bit_rate = 100000;//10000000-9000000 + (codec_context->width * codec_context->height)*0.75; - break; - case GSR_VIDEO_QUALITY_VERY_HIGH: - codec_context->qmin = quality; - codec_context->qmax = quality; - codec_context->bit_rate = 100000;//10000000-9000000 + (codec_context->width * codec_context->height)*0.75; - break; - case GSR_VIDEO_QUALITY_ULTRA: - codec_context->qmin = quality; - codec_context->qmax = quality; - codec_context->bit_rate = 100000;//10000000-9000000 + (codec_context->width * codec_context->height)*0.75; - break; - } - - codec_context->rc_max_rate = codec_context->bit_rate; - //codec_context->rc_min_rate = codec_context->bit_rate; - codec_context->rc_buffer_size = codec_context->bit_rate;//codec_context->bit_rate / 10; - codec_context->rc_initial_buffer_occupancy = codec_context->bit_rate;//codec_context->bit_rate * 1000; - } else { - //codec_context->rc_buffer_size = 50000 * 1000; - } - //codec_context->profile = FF_PROFILE_H264_MAIN; - if (codec_context->codec_id == AV_CODEC_ID_MPEG1VIDEO) - codec_context->mb_decision = 2; - - const bool uses_vaapi_encoder = !use_software_video_encoder && egl.gpu_info.vendor != GSR_GPU_VENDOR_NVIDIA && !video_codec_is_vulkan(arg_parser.video_codec); - if(uses_vaapi_encoder && arg_parser.bitrate_mode != GSR_BITRATE_MODE_CBR) { - // 8 bit / 10 bit = 80%, and increase it even more - const float quality_multiply = hdr ? (8.0f/10.0f * 0.7f) : 1.0f; - codec_context->global_quality = video_quality_to_codec_quality_value(codec_context->codec_id, arg_parser.video_quality) * quality_multiply; - } - - av_opt_set_int(codec_context->priv_data, "b_ref_mode", 0, 0); - //av_opt_set_int(codec_context->priv_data, "cbr", true, 0); - - if(egl.gpu_info.vendor != GSR_GPU_VENDOR_NVIDIA || video_codec_is_vulkan(arg_parser.video_codec)) { - // TODO: More options, better options - //codec_context->bit_rate = codec_context->width * codec_context->height; - switch(arg_parser.bitrate_mode) { - case GSR_BITRATE_MODE_QP: { - if(video_codec_is_vulkan(arg_parser.video_codec)) - av_opt_set(codec_context->priv_data, "rc_mode", "cqp", 0); - else if(egl.gpu_info.vendor == GSR_GPU_VENDOR_NVIDIA) - av_opt_set(codec_context->priv_data, "rc", "constqp", 0); - else - av_opt_set(codec_context->priv_data, "rc_mode", "CQP", 0); - break; - } - case GSR_BITRATE_MODE_VBR: { - if(video_codec_is_vulkan(arg_parser.video_codec)) - av_opt_set(codec_context->priv_data, "rc_mode", "vbr", 0); - else if(egl.gpu_info.vendor == GSR_GPU_VENDOR_NVIDIA) - av_opt_set(codec_context->priv_data, "rc", "vbr", 0); - else - av_opt_set(codec_context->priv_data, "rc_mode", "VBR", 0); - break; - } - case GSR_BITRATE_MODE_CBR: { - if(video_codec_is_vulkan(arg_parser.video_codec)) - av_opt_set(codec_context->priv_data, "rc_mode", "cbr", 0); - else if(egl.gpu_info.vendor == GSR_GPU_VENDOR_NVIDIA) - av_opt_set(codec_context->priv_data, "rc", "cbr", 0); - else - av_opt_set(codec_context->priv_data, "rc_mode", "CBR", 0); - break; - } - } - //codec_context->global_quality = 4; - //codec_context->compression_level = 2; - } - - //av_opt_set(codec_context->priv_data, "bsf", "hevc_metadata=colour_primaries=9:transfer_characteristics=16:matrix_coefficients=9", 0); - - if(arg_parser.tune == GSR_TUNE_QUALITY) - codec_context->max_b_frames = 2; - - codec_context->flags |= AV_CODEC_FLAG_GLOBAL_HEADER; - - return codec_context; -} - -static void open_audio(AVCodecContext *audio_codec_context, const char *ffmpeg_audio_opts) { - AVDictionary *options = nullptr; - av_dict_set(&options, "strict", "experimental", 0); - - if(ffmpeg_audio_opts) - av_dict_parse_string(&options, ffmpeg_audio_opts, "=", ";", 0); - - int ret; - ret = avcodec_open2(audio_codec_context, audio_codec_context->codec, &options); - if(ret < 0) { - fprintf(stderr, "failed to open codec, reason: %s\n", av_error_to_string(ret)); - _exit(1); - } -} - -static AVFrame* create_audio_frame(AVCodecContext *audio_codec_context) { - AVFrame *frame = av_frame_alloc(); - if(!frame) { - fprintf(stderr, "failed to allocate audio frame\n"); - _exit(1); - } - - frame->sample_rate = audio_codec_context->sample_rate; - frame->nb_samples = audio_codec_context->frame_size; - frame->format = audio_codec_context->sample_fmt; -#if LIBAVCODEC_VERSION_MAJOR < 60 - frame->channels = audio_codec_context->channels; - frame->channel_layout = audio_codec_context->channel_layout; -#else - av_channel_layout_copy(&frame->ch_layout, &audio_codec_context->ch_layout); -#endif - - int ret = av_frame_get_buffer(frame, 0); - if(ret < 0) { - fprintf(stderr, "failed to allocate audio data buffers, reason: %s\n", av_error_to_string(ret)); - _exit(1); - } - - return frame; -} - -static void dict_set_profile(AVCodecContext *codec_context, gsr_gpu_vendor vendor, gsr_color_depth color_depth, gsr_video_codec video_codec, AVDictionary **options) { - #if LIBAVCODEC_VERSION_INT < AV_VERSION_INT(61, 17, 100) - if(codec_context->codec_id == AV_CODEC_ID_H264) { - // TODO: Only for vaapi - //if(color_depth == GSR_COLOR_DEPTH_10_BITS) - // av_dict_set(options, "profile", "high10", 0); - //else - av_dict_set(options, "profile", "high", 0); - } else if(codec_context->codec_id == AV_CODEC_ID_AV1) { - if(vendor == GSR_GPU_VENDOR_NVIDIA) { - if(color_depth == GSR_COLOR_DEPTH_10_BITS) - av_dict_set_int(options, "highbitdepth", 1, 0); - } else { - av_dict_set(options, "profile", "main", 0); // TODO: use professional instead? - } - } else if(codec_context->codec_id == AV_CODEC_ID_HEVC) { - if(color_depth == GSR_COLOR_DEPTH_10_BITS) - av_dict_set(options, "profile", "main10", 0); - else - av_dict_set(options, "profile", "main", 0); - } - #else - const bool use_nvidia_values = vendor == GSR_GPU_VENDOR_NVIDIA && !video_codec_is_vulkan(video_codec); - if(codec_context->codec_id == AV_CODEC_ID_H264) { - // TODO: Only for vaapi - //if(color_depth == GSR_COLOR_DEPTH_10_BITS) - // av_dict_set_int(options, "profile", AV_PROFILE_H264_HIGH_10, 0); - //else - av_dict_set_int(options, "profile", use_nvidia_values ? 2 : AV_PROFILE_H264_HIGH, 0); - } else if(codec_context->codec_id == AV_CODEC_ID_AV1) { - if(use_nvidia_values) { - if(color_depth == GSR_COLOR_DEPTH_10_BITS) - av_dict_set_int(options, "highbitdepth", 1, 0); - } else { - av_dict_set_int(options, "profile", AV_PROFILE_AV1_MAIN, 0); // TODO: use professional instead? - } - } else if(codec_context->codec_id == AV_CODEC_ID_HEVC) { - if(color_depth == GSR_COLOR_DEPTH_10_BITS) - av_dict_set_int(options, "profile", use_nvidia_values ? 1 : AV_PROFILE_HEVC_MAIN_10, 0); - else - av_dict_set_int(options, "profile", use_nvidia_values ? 0 : AV_PROFILE_HEVC_MAIN, 0); - } - #endif -} - -static void video_software_set_qp(AVCodecContext *codec_context, gsr_video_quality video_quality, bool hdr, AVDictionary **options) { - // 8 bit / 10 bit = 80% - const float qp_multiply = hdr ? 8.0f/10.0f : 1.0f; - av_dict_set_int(options, "qp", video_quality_to_codec_quality_value(codec_context->codec_id, video_quality) * qp_multiply, 0); -} - -static void open_video_software(AVCodecContext *codec_context, const args_parser &arg_parser) { - const bool hdr = video_codec_is_hdr(arg_parser.video_codec); - AVDictionary *options = nullptr; - - if(arg_parser.bitrate_mode == GSR_BITRATE_MODE_QP) - video_software_set_qp(codec_context, arg_parser.video_quality, hdr, &options); - - av_dict_set(&options, "preset", "veryfast", 0); - av_dict_set(&options, "tune", "film", 0); - av_dict_set_int(&options, "forced-idr", 1, 0); - - if(codec_context->codec_id == AV_CODEC_ID_H264) { - av_dict_set(&options, "coder", "cabac", 0); // TODO: cavlc is faster than cabac but worse compression. Which to use? - } - - av_dict_set(&options, "strict", "experimental", 0); - - if(arg_parser.ffmpeg_video_opts) - av_dict_parse_string(&options, arg_parser.ffmpeg_video_opts, "=", ";", 0); - - int ret = avcodec_open2(codec_context, codec_context->codec, &options); - if (ret < 0) { - gsr_log(GSR_LOG_LEVEL_ERROR, "Could not open video codec: %s", av_error_to_string(ret)); - _exit(1); - } -} - -static void video_set_rc(gsr_video_codec video_codec, gsr_gpu_vendor vendor, gsr_bitrate_mode bitrate_mode, AVDictionary **options) { - switch(bitrate_mode) { - case GSR_BITRATE_MODE_QP: { - if(video_codec_is_vulkan(video_codec)) - av_dict_set(options, "rc_mode", "cqp", 0); - else if(vendor == GSR_GPU_VENDOR_NVIDIA) - av_dict_set(options, "rc", "constqp", 0); - else - av_dict_set(options, "rc_mode", "CQP", 0); - break; - } - case GSR_BITRATE_MODE_VBR: { - if(video_codec_is_vulkan(video_codec)) - av_dict_set(options, "rc_mode", "vbr", 0); - else if(vendor == GSR_GPU_VENDOR_NVIDIA) - av_dict_set(options, "rc", "vbr", 0); - else - av_dict_set(options, "rc_mode", "VBR", 0); - break; - } - case GSR_BITRATE_MODE_CBR: { - if(video_codec_is_vulkan(video_codec)) - av_dict_set(options, "rc_mode", "cbr", 0); - else if(vendor == GSR_GPU_VENDOR_NVIDIA) - av_dict_set(options, "rc", "cbr", 0); - else - av_dict_set(options, "rc_mode", "CBR", 0); - break; - } - } -} - -static void video_hardware_set_qp(AVCodecContext *codec_context, gsr_video_quality video_quality, bool hdr, AVDictionary **options) { - // 8 bit / 10 bit = 80% - const float qp_multiply = hdr ? 8.0f/10.0f : 1.0f; - av_dict_set_int(options, "qp", video_quality_to_codec_quality_value(codec_context->codec_id, video_quality) * qp_multiply, 0); -} - -static void open_video_hardware(AVCodecContext *codec_context, bool low_power, const gsr_egl &egl, const args_parser &arg_parser) { - const gsr_color_depth color_depth = video_codec_to_bit_depth(arg_parser.video_codec); - const bool hdr = video_codec_is_hdr(arg_parser.video_codec); - AVDictionary *options = nullptr; - - if(arg_parser.bitrate_mode == GSR_BITRATE_MODE_QP) - video_hardware_set_qp(codec_context, arg_parser.video_quality, hdr, &options); - - video_set_rc(arg_parser.video_codec, egl.gpu_info.vendor, arg_parser.bitrate_mode, &options); - - // TODO: Enable multipass - - dict_set_profile(codec_context, egl.gpu_info.vendor, color_depth, arg_parser.video_codec, &options); - - if(video_codec_is_vulkan(arg_parser.video_codec)) { - av_dict_set_int(&options, "async_depth", 3, 0); - av_dict_set(&options, "tune", "ll", 0); // Low latency - av_dict_set(&options, "usage", arg_parser.is_livestream ? "stream" : "record", 0); - av_dict_set(&options, "content", "rendered", 0); // Game or 3D content - - if(codec_context->codec_id == AV_CODEC_ID_H264) { - // Removed because it causes stutter in games for some people - //av_dict_set_int(&options, "quality", 5, 0); // quality preset - } else if(codec_context->codec_id == AV_CODEC_ID_AV1) { - av_dict_set(&options, "tier", "main", 0); - } else if(codec_context->codec_id == AV_CODEC_ID_HEVC) { - if(hdr) - av_dict_set(&options, "sei", "hdr", 0); - } - } else if(egl.gpu_info.vendor == GSR_GPU_VENDOR_NVIDIA) { - // TODO: These dont seem to be necessary - // av_dict_set_int(&options, "zerolatency", 1, 0); - // if(codec_context->codec_id == AV_CODEC_ID_AV1) { - // av_dict_set(&options, "tune", "ll", 0); - // } else if(codec_context->codec_id == AV_CODEC_ID_H264 || codec_context->codec_id == AV_CODEC_ID_HEVC) { - // av_dict_set(&options, "preset", "llhq", 0); - // av_dict_set(&options, "tune", "ll", 0); - // } - av_dict_set(&options, "tune", "ll", 0); - av_dict_set_int(&options, "forced-idr", 1, 0); - - switch(arg_parser.tune) { - case GSR_TUNE_PERFORMANCE: - //av_dict_set(&options, "multipass", "qres", 0); - break; - case GSR_TUNE_QUALITY: - av_dict_set(&options, "multipass", "fullres", 0); - av_dict_set(&options, "preset", "p6", 0); - av_dict_set_int(&options, "rc-lookahead", 0, 0); - break; - } - - if(codec_context->codec_id == AV_CODEC_ID_H264) { - // TODO: h264 10bit? - // TODO: - // switch(pixel_format) { - // case GSR_PIXEL_FORMAT_YUV420: - // av_dict_set_int(&options, "profile", AV_PROFILE_H264_HIGH, 0); - // break; - // case GSR_PIXEL_FORMAT_YUV444: - // av_dict_set_int(&options, "profile", AV_PROFILE_H264_HIGH_444, 0); - // break; - // } - } else if(codec_context->codec_id == AV_CODEC_ID_AV1) { - switch(arg_parser.pixel_format) { - case GSR_PIXEL_FORMAT_YUV420: - av_dict_set(&options, "rgb_mode", "yuv420", 0); - break; - case GSR_PIXEL_FORMAT_YUV444: - av_dict_set(&options, "rgb_mode", "yuv444", 0); - break; - } - } else if(codec_context->codec_id == AV_CODEC_ID_HEVC) { - //av_dict_set(&options, "pix_fmt", "yuv420p16le", 0); - } - } else { - // TODO: More quality options - if(low_power) - av_dict_set_int(&options, "low_power", 1, 0); - // Improves performance but increases vram. - // TODO: Might need a different async_depth for optimal performance on different amd/intel gpus - av_dict_set_int(&options, "async_depth", 3, 0); - - if(codec_context->codec_id == AV_CODEC_ID_H264) { - // Removed because it causes stutter in games for some people - //av_dict_set_int(&options, "quality", 5, 0); // quality preset - } else if(codec_context->codec_id == AV_CODEC_ID_AV1) { - av_dict_set(&options, "tier", "main", 0); - } else if(codec_context->codec_id == AV_CODEC_ID_HEVC) { - if(hdr) - av_dict_set(&options, "sei", "hdr", 0); - } - - // TODO: vp8/vp9 10bit - } - - if(codec_context->codec_id == AV_CODEC_ID_H264) { - av_dict_set(&options, "coder", "cabac", 0); // TODO: cavlc is faster than cabac but worse compression. Which to use? - } - - av_dict_set(&options, "strict", "experimental", 0); - - if(arg_parser.ffmpeg_video_opts) - av_dict_parse_string(&options, arg_parser.ffmpeg_video_opts, "=", ";", 0); - - int ret = avcodec_open2(codec_context, codec_context->codec, &options); - if (ret < 0) { - gsr_log(GSR_LOG_LEVEL_ERROR, "Could not open video codec: %s", av_error_to_string(ret)); - _exit(1); - } -} - static const int save_replay_seconds_full = -1; static sig_atomic_t running = 1; @@ -860,22 +267,6 @@ static void run_recording_saved_script_async(const char *script_file, const char } } -static double audio_codec_get_desired_delay(gsr_audio_codec audio_codec, int fps) { - const double fps_inv = 1.0 / (double)fps; - const double base = 0.01 + 1.0/165.0; - switch(audio_codec) { - case GSR_AUDIO_CODEC_OPUS: - return std::max(0.0, base - fps_inv); - case GSR_AUDIO_CODEC_AAC: - return std::max(0.0, (base + 0.008) * 2.0 - fps_inv); - case GSR_AUDIO_CODEC_FLAC: - // TODO: Test - return std::max(0.0, base - fps_inv); - } - assert(false); - return std::max(0.0, base - fps_inv); -} - struct AudioDeviceData { SoundDevice sound_device; AudioInput audio_input; @@ -1013,7 +404,7 @@ struct VideoSource { static RecordingStartResult start_recording_create_streams(const char *filename, const args_parser &arg_parser, AVCodecContext *video_codec_context, const std::vector &audio_tracks, bool hdr, std::vector &video_sources) { AVFormatContext *av_format_context; - avformat_alloc_output_context2(&av_format_context, nullptr, arg_parser.container_format, filename); + avformat_alloc_output_context2(&av_format_context, nullptr, arg_parser.settings.container_format, filename); set_format_context_options(av_format_context); AVStream *video_stream = create_stream(av_format_context, video_codec_context); @@ -1024,7 +415,7 @@ static RecordingStartResult start_recording_create_streams(const char *filename, for(const AudioTrack &audio_track : audio_tracks) { AVStream *audio_stream = create_stream(av_format_context, audio_track.codec_context); - if(!audio_track.name.empty() && !arg_parser.exclude_metadata) + if(!audio_track.name.empty() && !arg_parser.settings.exclude_metadata) av_dict_set(&audio_stream->metadata, "title", audio_track.name.c_str(), 0); avcodec_parameters_from_context(audio_stream->codecpar, audio_track.codec_context); result.audio_inputs.push_back({&audio_track, audio_stream}); @@ -1032,20 +423,20 @@ static RecordingStartResult start_recording_create_streams(const char *filename, const int open_ret = avio_open(&av_format_context->pb, filename, AVIO_FLAG_WRITE); if(open_ret < 0) { - gsr_log(GSR_LOG_LEVEL_ERROR, "start_recording_create_streams: could not open '%s': %s", filename, av_error_to_string(open_ret)); + gsr_log(GSR_LOG_LEVEL_ERROR, "start_recording_create_streams: could not open '%s': %s", filename, gsr_av_error_to_string(open_ret)); return result; } AVDictionary *options = nullptr; av_dict_set(&options, "strict", "experimental", 0); - if(arg_parser.ffmpeg_opts) - av_dict_parse_string(&options, arg_parser.ffmpeg_opts, "=", ";", 0); + if(arg_parser.settings.ffmpeg_opts) + av_dict_parse_string(&options, arg_parser.settings.ffmpeg_opts, "=", ";", 0); const int header_write_ret = avformat_write_header(av_format_context, &options); av_dict_free(&options); if(header_write_ret < 0) { - gsr_log(GSR_LOG_LEVEL_ERROR, "start_recording_create_streams: error occurred when writing header to output file: %s", av_error_to_string(header_write_ret)); + gsr_log(GSR_LOG_LEVEL_ERROR, "start_recording_create_streams: error occurred when writing header to output file: %s", gsr_av_error_to_string(header_write_ret)); avio_close(av_format_context->pb); avformat_free_context(av_format_context); return result; @@ -1141,7 +532,7 @@ static bool save_replay_async(AVCodecContext *video_codec_context, int video_str audio_pts_offsets.push_back(AudioPtsOffset{audio_pts_offset, audio_track.stream_index}); } - std::string output_filepath = create_new_recording_filepath_from_timestamp(arg_parser.filename, "Replay", file_extension, date_folders); + std::string output_filepath = create_new_recording_filepath_from_timestamp(arg_parser.settings.filename, "Replay", file_extension, date_folders); RecordingStartResult recording_start_result = start_recording_create_streams(output_filepath.c_str(), arg_parser, video_codec_context, audio_tracks, hdr, video_sources); if(!recording_start_result.av_format_context) { pthread_mutex_lock(&encoder->replay_mutex); @@ -1218,7 +609,7 @@ static bool save_replay_async(AVCodecContext *video_codec_context, int video_str const int ret = av_write_frame(recording_start_result.av_format_context, &av_packet); if(ret < 0) - gsr_log(GSR_LOG_LEVEL_ERROR, "Failed to write frame index %d to muxer, reason: %s (%d)", av_packet.stream_index, av_error_to_string(ret), ret); + gsr_log(GSR_LOG_LEVEL_ERROR, "Failed to write frame index %d to muxer, reason: %s (%d)", av_packet.stream_index, gsr_av_error_to_string(ret), ret); free(replay_packet_data); @@ -1421,10 +812,10 @@ fail: } static gsr_video_encoder* create_video_encoder(gsr_egl *egl, const args_parser &arg_parser) { - const gsr_color_depth color_depth = video_codec_to_bit_depth(arg_parser.video_codec); + const gsr_color_depth color_depth = video_codec_to_bit_depth(arg_parser.settings.video_codec); gsr_video_encoder *video_encoder = nullptr; - if(arg_parser.video_encoder == GSR_VIDEO_ENCODER_HW_CPU) { + if(arg_parser.settings.video_encoder == GSR_VIDEO_ENCODER_HW_CPU) { gsr_video_encoder_software_params params; params.egl = egl; params.color_depth = color_depth; @@ -1432,7 +823,7 @@ static gsr_video_encoder* create_video_encoder(gsr_egl *egl, const args_parser & return video_encoder; } - if(video_codec_is_vulkan(arg_parser.video_codec)) { + if(video_codec_is_vulkan(arg_parser.settings.video_codec)) { gsr_video_encoder_vulkan_params params; params.egl = egl; params.color_depth = color_depth; @@ -2020,8 +1411,8 @@ static gsr_capture* create_monitor_capture(const args_parser &arg_parser, gsr_eg ximage_params.egl = egl; ximage_params.cursor = &x11_cursor; ximage_params.display_to_capture = capture_source.name.c_str(); - ximage_params.record_cursor = arg_parser.record_cursor; - ximage_params.output_resolution = arg_parser.output_resolution; + ximage_params.record_cursor = arg_parser.settings.record_cursor; + ximage_params.output_resolution = arg_parser.settings.output_resolution; ximage_params.region_size = capture_source.region_size; ximage_params.region_position = capture_source.region_pos; return gsr_capture_ximage_create(&ximage_params); @@ -2047,10 +1438,10 @@ static gsr_capture* create_monitor_capture(const args_parser &arg_parser, gsr_eg kms_params.kms_response = &kms_response; kms_params.kde_night_light = kde_night_light; kms_params.display_to_capture = capture_source.name.c_str(); - kms_params.record_cursor = arg_parser.record_cursor; - kms_params.hdr = video_codec_is_hdr(arg_parser.video_codec); - kms_params.fps = arg_parser.fps; - kms_params.output_resolution = arg_parser.output_resolution; + kms_params.record_cursor = arg_parser.settings.record_cursor; + kms_params.hdr = video_codec_is_hdr(arg_parser.settings.video_codec); + kms_params.fps = arg_parser.settings.fps; + kms_params.output_resolution = arg_parser.settings.output_resolution; kms_params.region_size = capture_source.region_size; kms_params.region_position = capture_source.region_pos; return gsr_capture_kms_create(&kms_params); @@ -2066,10 +1457,10 @@ static gsr_capture* create_monitor_capture(const args_parser &arg_parser, gsr_eg memset(&nvfbc_params, 0, sizeof(nvfbc_params)); nvfbc_params.egl = egl; nvfbc_params.display_to_capture = capture_source_real; - nvfbc_params.fps = arg_parser.fps; + nvfbc_params.fps = arg_parser.settings.fps; nvfbc_params.direct_capture = direct_capture; - nvfbc_params.record_cursor = arg_parser.record_cursor; - nvfbc_params.output_resolution = arg_parser.output_resolution; + nvfbc_params.record_cursor = arg_parser.settings.record_cursor; + nvfbc_params.output_resolution = arg_parser.settings.output_resolution; nvfbc_params.region_size = capture_source.region_size; nvfbc_params.region_position = capture_source.region_pos; return gsr_capture_nvfbc_create(&nvfbc_params); @@ -2126,8 +1517,8 @@ static gsr_capture* create_capture_impl(const args_parser &arg_parser, gsr_egl * _exit(2); } - if(arg_parser.output_resolution.x <= 0 || arg_parser.output_resolution.y <= 0) { - gsr_log(GSR_LOG_LEVEL_ERROR, "invalid value for option -s '%dx%d' when using -w focused option. expected width and height to be greater than 0", arg_parser.output_resolution.x, arg_parser.output_resolution.y); + if(arg_parser.settings.output_resolution.x <= 0 || arg_parser.settings.output_resolution.y <= 0) { + gsr_log(GSR_LOG_LEVEL_ERROR, "invalid value for option -s '%dx%d' when using -w focused option. expected width and height to be greater than 0", arg_parser.settings.output_resolution.x, arg_parser.settings.output_resolution.y); args_parser_print_usage(); _exit(1); } @@ -2144,10 +1535,10 @@ static gsr_capture* create_capture_impl(const args_parser &arg_parser, gsr_egl * gsr_capture_portal_params portal_params; memset(&portal_params, 0, sizeof(portal_params)); portal_params.egl = egl; - portal_params.record_cursor = arg_parser.record_cursor; - portal_params.restore_portal_session = arg_parser.restore_portal_session; - portal_params.portal_session_token_filepath = arg_parser.portal_session_token_filepath; - portal_params.output_resolution = arg_parser.output_resolution; + portal_params.record_cursor = arg_parser.settings.record_cursor; + portal_params.restore_portal_session = arg_parser.settings.restore_portal_session; + portal_params.portal_session_token_filepath = arg_parser.settings.portal_session_token_filepath; + portal_params.output_resolution = arg_parser.settings.output_resolution; capture = gsr_capture_portal_create(&portal_params); if(!capture) _exit(1); @@ -2169,7 +1560,7 @@ static gsr_capture* create_capture_impl(const args_parser &arg_parser, gsr_egl * gsr_capture_v4l2_params v4l2_params; memset(&v4l2_params, 0, sizeof(v4l2_params)); v4l2_params.egl = egl; - v4l2_params.output_resolution = arg_parser.output_resolution; + v4l2_params.output_resolution = arg_parser.settings.output_resolution; v4l2_params.device_path = capture_source.name.c_str(); v4l2_params.pixfmt = capture_source.v4l2_pixfmt; v4l2_params.camera_fps = capture_source.camera_fps; @@ -2192,8 +1583,8 @@ static gsr_capture* create_capture_impl(const args_parser &arg_parser, gsr_egl * xcomposite_params.cursor = &x11_cursor; xcomposite_params.window = capture_source.window_id; xcomposite_params.follow_focused = follow_focused; - xcomposite_params.record_cursor = arg_parser.record_cursor; - xcomposite_params.output_resolution = arg_parser.output_resolution; + xcomposite_params.record_cursor = arg_parser.settings.record_cursor; + xcomposite_params.output_resolution = arg_parser.settings.output_resolution; capture = gsr_capture_xcomposite_create(&xcomposite_params); if(!capture) _exit(1); @@ -2241,7 +1632,7 @@ static std::vector create_video_sources(const args_parser &arg_pars for(CaptureSource &capture_source : capture_sources) { gsr_capture_metadata capture_metadata; memset(&capture_metadata, 0, sizeof(capture_metadata)); - capture_metadata.fps = arg_parser.fps; + capture_metadata.fps = arg_parser.settings.fps; capture_metadata.halign = capture_source.halign; capture_metadata.valign = capture_source.valign; capture_metadata.flip = (gsr_flip)capture_source.flip; @@ -2340,13 +1731,13 @@ static void load_plugins(gsr_plugins *plugins, args_parser &arg_parser, gsr_egl assert(plugin_arg); if(plugin_arg->num_values > 0) { - const gsr_color_depth color_depth = video_codec_to_bit_depth(arg_parser.video_codec); + const gsr_color_depth color_depth = video_codec_to_bit_depth(arg_parser.settings.video_codec); assert(color_depth == GSR_COLOR_DEPTH_8_BITS || color_depth == GSR_COLOR_DEPTH_10_BITS); const gsr_plugin_init_params plugin_init_params = { (unsigned int)video_size.x, (unsigned int)video_size.y, - (unsigned int)arg_parser.fps, + (unsigned int)arg_parser.settings.fps, color_depth == GSR_COLOR_DEPTH_8_BITS ? GSR_PLUGIN_COLOR_DEPTH_8_BITS : GSR_PLUGIN_COLOR_DEPTH_10_BITS, egl->context_type == GSR_GL_CONTEXT_TYPE_GLX ? GSR_PLUGIN_GRAPHICS_API_GLX : GSR_PLUGIN_GRAPHICS_API_EGL_ES, }; @@ -2363,9 +1754,9 @@ static void load_plugins(gsr_plugins *plugins, args_parser &arg_parser, gsr_egl // TODO: 10-bit and hdr. static void capture_image_to_file(args_parser &arg_parser, gsr_egl *egl, gsr_window *window, gsr_image_format image_format, std::vector &capture_sources) { - const int image_quality = video_quality_to_image_quality_value(arg_parser.video_quality); + const int image_quality = video_quality_to_image_quality_value(arg_parser.settings.video_quality); const gsr_color_range color_range = image_format_to_color_range(image_format, image_quality); - arg_parser.fps = 60; // We want to capture an image as soon as possible + arg_parser.settings.fps = 60; // We want to capture an image as soon as possible vec2i video_size = {0, 0}; std::vector video_sources = create_video_sources(arg_parser, egl, true, capture_sources, video_size); @@ -2412,7 +1803,7 @@ static void capture_image_to_file(args_parser &arg_parser, gsr_egl *egl, gsr_win while(running) { while(gsr_window_process_event(window)) { - if(x11_cursor_display && arg_parser.record_cursor) + if(x11_cursor_display && arg_parser.settings.record_cursor) gsr_cursor_on_event(&x11_cursor, gsr_window_get_event_data(window)); for(VideoSource &video_source : video_sources) { @@ -2420,7 +1811,7 @@ static void capture_image_to_file(args_parser &arg_parser, gsr_egl *egl, gsr_win } } - if(x11_cursor_display && arg_parser.record_cursor) + if(x11_cursor_display && arg_parser.settings.record_cursor) gsr_cursor_tick(&x11_cursor, DefaultRootWindow(x11_cursor_display)); gsr_capture_kms_cleanup_kms_fds(); @@ -2477,13 +1868,13 @@ static void capture_image_to_file(args_parser &arg_parser, gsr_egl *egl, gsr_win gsr_egl_swap_buffers(egl); if(!should_stop_error) { - if(!gsr_image_writer_write_to_file(&image_writer, arg_parser.filename, image_format, image_quality)) { - gsr_log(GSR_LOG_LEVEL_ERROR, "capture_image_to_file: failed to write opengl texture to image output file %s", arg_parser.filename); + if(!gsr_image_writer_write_to_file(&image_writer, arg_parser.settings.filename, image_format, image_quality)) { + gsr_log(GSR_LOG_LEVEL_ERROR, "capture_image_to_file: failed to write opengl texture to image output file %s", arg_parser.settings.filename); _exit(1); } - if(arg_parser.recording_saved_script) - run_recording_saved_script_async(arg_parser.recording_saved_script, arg_parser.filename, "screenshot"); + if(arg_parser.settings.recording_saved_script) + run_recording_saved_script_async(arg_parser.settings.recording_saved_script, arg_parser.settings.filename, "screenshot"); } gsr_plugins_deinit(&plugins); @@ -2494,17 +1885,6 @@ static void capture_image_to_file(args_parser &arg_parser, gsr_egl *egl, gsr_win _exit(should_stop_error ? 3 : 0); } -static AVPixelFormat get_pixel_format(gsr_video_codec video_codec, gsr_gpu_vendor vendor, bool use_software_video_encoder) { - if(use_software_video_encoder) { - return AV_PIX_FMT_NV12; - } else { - if(video_codec_is_vulkan(video_codec)) - return AV_PIX_FMT_VULKAN; - else - return vendor == GSR_GPU_VENDOR_NVIDIA ? AV_PIX_FMT_CUDA : AV_PIX_FMT_VAAPI; - } -} - static void match_app_audio_input_to_available_apps(const std::vector &requested_audio_inputs, const std::vector &app_audio_names) { for(const AudioInput &request_audio_input : requested_audio_inputs) { if(request_audio_input.type != AudioInputType::APPLICATION || request_audio_input.inverted) @@ -2872,8 +2252,8 @@ static bool parse_capture_source_arg_callback(const char *sub, size_t size, void capture_source_size = capture_source_end - sub; CaptureSource capture_source; - capture_source.region_pos = parse_userdata->arg_parser->region_position; - capture_source.region_size = parse_userdata->arg_parser->region_size; + capture_source.region_pos = parse_userdata->arg_parser->settings.region_position; + capture_source.region_size = parse_userdata->arg_parser->settings.region_size; if(gsr_string_starts_with(sub, capture_source_size, "monitor:")) { capture_source.type = GSR_CAPTURE_SOURCE_TYPE_MONITOR; @@ -3181,24 +2561,24 @@ static void print_codec_error(gsr_video_codec video_codec) { } static void force_cpu_encoding(args_parser *args_parser) { - args_parser->video_codec = GSR_VIDEO_CODEC_H264; - args_parser->video_encoder = GSR_VIDEO_ENCODER_HW_CPU; - if(args_parser->bitrate_mode == GSR_BITRATE_MODE_VBR) { + args_parser->settings.video_codec = GSR_VIDEO_CODEC_H264; + args_parser->settings.video_encoder = GSR_VIDEO_ENCODER_HW_CPU; + if(args_parser->settings.bitrate_mode == GSR_BITRATE_MODE_VBR) { gsr_log(GSR_LOG_LEVEL_WARNING, "bitrate mode has been forcefully set to qp because software encoding option doesn't support vbr option"); - args_parser->bitrate_mode = GSR_BITRATE_MODE_QP; + args_parser->settings.bitrate_mode = GSR_BITRATE_MODE_QP; } } static const AVCodec* pick_video_codec(gsr_egl *egl, args_parser *args_parser, bool use_fallback_codec, bool *low_power, gsr_supported_video_codecs *supported_video_codecs) { // TODO: software encoder for hevc, av1, vp8 and vp9 *low_power = false; - const AVCodec *video_codec_f = get_av_codec_if_supported(args_parser->video_codec, egl, args_parser->video_encoder == GSR_VIDEO_ENCODER_HW_CPU, supported_video_codecs); + const AVCodec *video_codec_f = get_av_codec_if_supported(args_parser->settings.video_codec, egl, args_parser->settings.video_encoder == GSR_VIDEO_ENCODER_HW_CPU, supported_video_codecs); - if(!video_codec_f && use_fallback_codec && args_parser->video_encoder != GSR_VIDEO_ENCODER_HW_CPU) { - switch(args_parser->video_codec) { + if(!video_codec_f && use_fallback_codec && args_parser->settings.video_encoder != GSR_VIDEO_ENCODER_HW_CPU) { + switch(args_parser->settings.video_codec) { case GSR_VIDEO_CODEC_H264: { gsr_log(GSR_LOG_LEVEL_ERROR, "selected video codec h264 is not supported by your hardware"); - if(args_parser->fallback_cpu_encoding) { + if(args_parser->settings.fallback_cpu_encoding) { gsr_log(GSR_LOG_LEVEL_WARNING, "gpu encoding is not available on your system, trying cpu encoding instead because -fallback-cpu-encoding is enabled. Install the proper vaapi drivers on your system (if supported) if you experience performance issues"); force_cpu_encoding(args_parser); } @@ -3208,14 +2588,14 @@ static const AVCodec* pick_video_codec(gsr_egl *egl, args_parser *args_parser, b case GSR_VIDEO_CODEC_HEVC_HDR: case GSR_VIDEO_CODEC_HEVC_10BIT: { gsr_log(GSR_LOG_LEVEL_WARNING, "selected video codec hevc is not supported by your hardware, trying h264 instead"); - args_parser->video_codec = GSR_VIDEO_CODEC_H264; + args_parser->settings.video_codec = GSR_VIDEO_CODEC_H264; return pick_video_codec(egl, args_parser, true, low_power, supported_video_codecs); } case GSR_VIDEO_CODEC_AV1: case GSR_VIDEO_CODEC_AV1_HDR: case GSR_VIDEO_CODEC_AV1_10BIT: { gsr_log(GSR_LOG_LEVEL_WARNING, "selected video codec av1 is not supported by your hardware, trying h264 instead"); - args_parser->video_codec = GSR_VIDEO_CODEC_H264; + args_parser->settings.video_codec = GSR_VIDEO_CODEC_H264; return pick_video_codec(egl, args_parser, true, low_power, supported_video_codecs); } case GSR_VIDEO_CODEC_VP8: @@ -3224,11 +2604,11 @@ static const AVCodec* pick_video_codec(gsr_egl *egl, args_parser *args_parser, b break; case GSR_VIDEO_CODEC_H264_VULKAN: { gsr_log(GSR_LOG_LEVEL_WARNING, "selected video codec h264_vulkan is not supported by your hardware, trying h264 instead"); - args_parser->video_codec = GSR_VIDEO_CODEC_H264; + args_parser->settings.video_codec = GSR_VIDEO_CODEC_H264; // Need to do a query again because this time it's without vulkan - if(!get_supported_video_codecs(egl, args_parser->video_codec, false, true, supported_video_codecs)) { + if(!get_supported_video_codecs(egl, args_parser->settings.video_codec, false, true, supported_video_codecs)) { gsr_log(GSR_LOG_LEVEL_ERROR, "failed to query for supported video codecs"); - print_codec_error(args_parser->video_codec); + print_codec_error(args_parser->settings.video_codec); _exit(11); } return pick_video_codec(egl, args_parser, true, low_power, supported_video_codecs); @@ -3237,11 +2617,11 @@ static const AVCodec* pick_video_codec(gsr_egl *egl, args_parser *args_parser, b case GSR_VIDEO_CODEC_HEVC_HDR_VULKAN: case GSR_VIDEO_CODEC_HEVC_10BIT_VULKAN: { gsr_log(GSR_LOG_LEVEL_WARNING, "selected video codec hevc_vulkan is not supported by your hardware, trying hevc instead"); - args_parser->video_codec = GSR_VIDEO_CODEC_HEVC; + args_parser->settings.video_codec = GSR_VIDEO_CODEC_HEVC; // Need to do a query again because this time it's without vulkan - if(!get_supported_video_codecs(egl, args_parser->video_codec, false, true, supported_video_codecs)) { + if(!get_supported_video_codecs(egl, args_parser->settings.video_codec, false, true, supported_video_codecs)) { gsr_log(GSR_LOG_LEVEL_ERROR, "failed to query for supported video codecs"); - print_codec_error(args_parser->video_codec); + print_codec_error(args_parser->settings.video_codec); _exit(11); } return pick_video_codec(egl, args_parser, true, low_power, supported_video_codecs); @@ -3250,26 +2630,26 @@ static const AVCodec* pick_video_codec(gsr_egl *egl, args_parser *args_parser, b case GSR_VIDEO_CODEC_AV1_HDR_VULKAN: case GSR_VIDEO_CODEC_AV1_10BIT_VULKAN: { gsr_log(GSR_LOG_LEVEL_WARNING, "selected video codec av1_vulkan is not supported by your hardware, trying av1 instead"); - args_parser->video_codec = GSR_VIDEO_CODEC_AV1; + args_parser->settings.video_codec = GSR_VIDEO_CODEC_AV1; // Need to do a query again because this time it's without vulkan - if(!get_supported_video_codecs(egl, args_parser->video_codec, false, true, supported_video_codecs)) { + if(!get_supported_video_codecs(egl, args_parser->settings.video_codec, false, true, supported_video_codecs)) { gsr_log(GSR_LOG_LEVEL_ERROR, "failed to query for supported video codecs"); - print_codec_error(args_parser->video_codec); + print_codec_error(args_parser->settings.video_codec); _exit(11); } return pick_video_codec(egl, args_parser, true, low_power, supported_video_codecs); } } - video_codec_f = get_av_codec_if_supported(args_parser->video_codec, egl, args_parser->video_encoder == GSR_VIDEO_ENCODER_HW_CPU, supported_video_codecs); + video_codec_f = get_av_codec_if_supported(args_parser->settings.video_codec, egl, args_parser->settings.video_encoder == GSR_VIDEO_ENCODER_HW_CPU, supported_video_codecs); } if(!video_codec_f) { - print_codec_error(args_parser->video_codec); + print_codec_error(args_parser->settings.video_codec); _exit(54); } - *low_power = video_codec_only_supports_low_power_mode(*supported_video_codecs, args_parser->video_codec); + *low_power = video_codec_only_supports_low_power_mode(*supported_video_codecs, args_parser->settings.video_codec); return video_codec_f; } @@ -3294,27 +2674,27 @@ static gsr_video_codec select_appropriate_video_codec_automatically(vec2i video_ static const AVCodec* select_video_codec_with_fallback(vec2i video_size, args_parser *args_parser, const char *file_extension, gsr_egl *egl, bool *low_power) { gsr_supported_video_codecs supported_video_codecs_non_vulkan; - get_supported_video_codecs(egl, args_parser->video_codec, args_parser->video_encoder == GSR_VIDEO_ENCODER_HW_CPU, true, &supported_video_codecs_non_vulkan); + get_supported_video_codecs(egl, args_parser->settings.video_codec, args_parser->settings.video_encoder == GSR_VIDEO_ENCODER_HW_CPU, true, &supported_video_codecs_non_vulkan); gsr_supported_video_codecs supported_video_codecs_vulkan = supported_video_codecs_non_vulkan; set_supported_video_codecs_ffmpeg(&supported_video_codecs_non_vulkan, &supported_video_codecs_vulkan, egl->gpu_info.vendor); - gsr_supported_video_codecs *supported_video_codecs = video_codec_is_vulkan(args_parser->video_codec) + gsr_supported_video_codecs *supported_video_codecs = video_codec_is_vulkan(args_parser->settings.video_codec) ? &supported_video_codecs_vulkan : &supported_video_codecs_non_vulkan; - const bool video_codec_auto = args_parser->video_codec == (gsr_video_codec)GSR_VIDEO_CODEC_AUTO; + const bool video_codec_auto = args_parser->settings.video_codec == (gsr_video_codec)GSR_VIDEO_CODEC_AUTO; if(video_codec_auto) { if(strcmp(file_extension, "webm") == 0) { gsr_log(GSR_LOG_LEVEL_INFO, "using vp8 encoder because a codec was not specified and the file extension is .webm"); - args_parser->video_codec = GSR_VIDEO_CODEC_VP8; - } else if(args_parser->video_encoder == GSR_VIDEO_ENCODER_HW_CPU) { + args_parser->settings.video_codec = GSR_VIDEO_CODEC_VP8; + } else if(args_parser->settings.video_encoder == GSR_VIDEO_ENCODER_HW_CPU) { gsr_log(GSR_LOG_LEVEL_INFO, "using h264 encoder because a codec was not specified"); - args_parser->video_codec = GSR_VIDEO_CODEC_H264; - } else if(args_parser->video_encoder != GSR_VIDEO_ENCODER_HW_CPU) { - args_parser->video_codec = select_appropriate_video_codec_automatically(video_size, &supported_video_codecs_non_vulkan); - if(args_parser->video_codec == (gsr_video_codec)-1) { - if(args_parser->fallback_cpu_encoding) { + args_parser->settings.video_codec = GSR_VIDEO_CODEC_H264; + } else if(args_parser->settings.video_encoder != GSR_VIDEO_ENCODER_HW_CPU) { + args_parser->settings.video_codec = select_appropriate_video_codec_automatically(video_size, &supported_video_codecs_non_vulkan); + if(args_parser->settings.video_codec == (gsr_video_codec)-1) { + if(args_parser->settings.fallback_cpu_encoding) { gsr_log(GSR_LOG_LEVEL_WARNING, "gpu encoding is not available on your system or your gpu doesn't support recording at the resolution you are trying to record, trying cpu encoding instead because -fallback-cpu-encoding is enabled. Install the proper vaapi drivers on your system (if supported) if you experience performance issues"); force_cpu_encoding(args_parser); } else { @@ -3327,22 +2707,22 @@ static const AVCodec* select_video_codec_with_fallback(vec2i video_size, args_pa } if(LIBAVFORMAT_VERSION_INT < AV_VERSION_INT(60, 10, 100) && strcmp(file_extension, "flv") == 0) { - if(args_parser->video_codec != GSR_VIDEO_CODEC_H264) { - args_parser->video_codec = GSR_VIDEO_CODEC_H264; + if(args_parser->settings.video_codec != GSR_VIDEO_CODEC_H264) { + args_parser->settings.video_codec = GSR_VIDEO_CODEC_H264; gsr_log(GSR_LOG_LEVEL_WARNING, "hevc/av1 is not compatible with flv in your outdated version of ffmpeg, falling back to h264 instead."); } } else if(strcmp(file_extension, "m3u8") == 0) { - if(video_codec_is_av1(args_parser->video_codec)) { - args_parser->video_codec = GSR_VIDEO_CODEC_HEVC; + if(video_codec_is_av1(args_parser->settings.video_codec)) { + args_parser->settings.video_codec = GSR_VIDEO_CODEC_HEVC; gsr_log(GSR_LOG_LEVEL_WARNING, "av1 is not compatible with hls (m3u8), falling back to hevc instead."); } } const AVCodec *codec = pick_video_codec(egl, args_parser, true, low_power, supported_video_codecs); - const vec2i codec_max_resolution = codec_get_max_resolution(args_parser->video_codec, args_parser->video_encoder == GSR_VIDEO_ENCODER_HW_CPU, supported_video_codecs); + const vec2i codec_max_resolution = codec_get_max_resolution(args_parser->settings.video_codec, args_parser->settings.video_encoder == GSR_VIDEO_ENCODER_HW_CPU, supported_video_codecs); if(!codec_supports_resolution(codec_max_resolution, video_size)) { - const char *video_codec_name = video_codec_to_string(args_parser->video_codec); + const char *video_codec_name = video_codec_to_string(args_parser->settings.video_codec); gsr_log(GSR_LOG_LEVEL_ERROR, "The max resolution for video codec %s is %dx%d while you are trying to capture at resolution %dx%d. Change capture resolution or video codec and try again", video_codec_name, codec_max_resolution.x, codec_max_resolution.y, video_size.x, video_size.y); _exit(53); } @@ -3374,6 +2754,8 @@ static std::vector create_device_audio_inputs(const std::vector } audio_device.frame = create_audio_frame(audio_codec_context); + if(!audio_device.frame) + _exit(1); audio_device.frame->pts = -audio_codec_context->frame_size * num_audio_frames_shift; audio_track_audio_devices.push_back(std::move(audio_device)); @@ -3385,6 +2767,8 @@ static std::vector create_device_audio_inputs(const std::vector static AudioDeviceData create_application_audio_audio_input(const MergedAudioInputs &merged_audio_inputs, AVCodecContext *audio_codec_context, int num_channels, double num_audio_frames_shift, gsr_pipewire_audio *pipewire_audio) { AudioDeviceData audio_device; audio_device.frame = create_audio_frame(audio_codec_context); + if(!audio_device.frame) + _exit(1); audio_device.frame->pts = -audio_codec_context->frame_size * num_audio_frames_shift; char random_str[8]; @@ -3461,30 +2845,18 @@ static void av_write_header(AVFormatContext *av_format_context, const char *ffmp const int ret = avformat_write_header(av_format_context, &options); if(ret < 0) - fprintf(stderr, "Error occurred when writing header to output file: %s\n", av_error_to_string(ret)); + fprintf(stderr, "Error occurred when writing header to output file: %s\n", gsr_av_error_to_string(ret)); av_dict_free(&options); } -static int audio_codec_get_frame_size(gsr_audio_codec audio_codec) { - switch(audio_codec) { - case GSR_AUDIO_CODEC_AAC: return 1024; - case GSR_AUDIO_CODEC_OPUS: return 960; - case GSR_AUDIO_CODEC_FLAC: - assert(false); - return 1024; - } - assert(false); - return 1024; -} - static size_t calculate_estimated_replay_buffer_packets(int64_t replay_buffer_size_secs, int fps, gsr_audio_codec audio_codec, const std::vector &audio_inputs) { if(replay_buffer_size_secs == -1) return 0; int audio_fps = 0; if(!audio_inputs.empty()) - audio_fps = AUDIO_SAMPLE_RATE / audio_codec_get_frame_size(audio_codec); + audio_fps = GSR_AUDIO_SAMPLE_RATE / audio_codec_get_frame_size(audio_codec); return replay_buffer_size_secs * (fps + audio_fps * audio_inputs.size()); } @@ -3566,7 +2938,7 @@ static void validate_args_with_capture_sources(args_parser &arg_parser, const st } } - if(!arg_parser.restore_portal_session && is_capturing_type(capture_sources, GSR_CAPTURE_SOURCE_TYPE_PORTAL)) + if(!arg_parser.settings.restore_portal_session && is_capturing_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"); } @@ -3670,7 +3042,7 @@ int main(int argc, char **argv) { if(!args_parser_parse(&arg_parser, argc, argv, &arg_handlers, NULL)) _exit(1); - if(!arg_parser.low_power) { + 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. @@ -3679,7 +3051,7 @@ int main(int argc, char **argv) { setenv("AMD_DEBUG", "lowlatencyenc", true); } - std::vector capture_sources = parse_capture_source_arg(arg_parser.capture_source, arg_parser); + std::vector capture_sources = parse_capture_source_arg(arg_parser.settings.capture_source, arg_parser); if(capture_sources.empty()) { 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(); @@ -3766,19 +3138,19 @@ int main(int argc, char **argv) { disable_prime_run(); } - if(video_codec_is_hdr(arg_parser.video_codec)) { + 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.video_codec = hdr_video_codec_to_sdr_video_codec(arg_parser.video_codec); + arg_parser.settings.video_codec = hdr_video_codec_to_sdr_video_codec(arg_parser.settings.video_codec); } } gsr_egl egl; - if(!gsr_egl_load(&egl, window, is_capturing_monitor_or_region(capture_sources), arg_parser.gl_debug)) { + if(!gsr_egl_load(&egl, window, is_capturing_monitor_or_region(capture_sources), arg_parser.settings.gl_debug)) { gsr_log(GSR_LOG_LEVEL_ERROR, "failed to load opengl"); _exit(1); } - gsr_shader_enable_debug_output(arg_parser.gl_debug); + gsr_shader_enable_debug_output(arg_parser.settings.gl_debug); #ifndef NDEBUG gsr_shader_enable_debug_output(true); #endif @@ -3799,7 +3171,7 @@ int main(int argc, char **argv) { memset(&x11_cursor, 0, sizeof(x11_cursor)); x11_cursor_display = NULL; - if(gsr_window_get_display_server(window) == GSR_DISPLAY_SERVER_X11 && arg_parser.record_cursor) { + if(gsr_window_get_display_server(window) == GSR_DISPLAY_SERVER_X11 && arg_parser.settings.record_cursor) { x11_cursor_display = (Display*)gsr_window_get_display(egl.window); gsr_cursor_init(&x11_cursor, &egl, x11_cursor_display); } @@ -3810,7 +3182,7 @@ int main(int argc, char **argv) { // } gsr_image_format image_format; - if(get_image_format_from_filename(arg_parser.filename, &image_format)) { + if(get_image_format_from_filename(arg_parser.settings.filename, &image_format)) { if(audio_input_arg->num_values > 0) { gsr_log(GSR_LOG_LEVEL_ERROR, "can't record audio (-a) when taking a screenshot"); _exit(1); @@ -3822,10 +3194,10 @@ int main(int argc, char **argv) { AVFormatContext *av_format_context; // The output format is automatically guessed by the file extension - avformat_alloc_output_context2(&av_format_context, nullptr, arg_parser.container_format, arg_parser.filename); + avformat_alloc_output_context2(&av_format_context, nullptr, arg_parser.settings.container_format, arg_parser.settings.filename); if (!av_format_context) { - if(arg_parser.container_format) { - gsr_log(GSR_LOG_LEVEL_ERROR, "Container format '%s' (argument -c) is not valid", arg_parser.container_format); + if(arg_parser.settings.container_format) { + gsr_log(GSR_LOG_LEVEL_ERROR, "Container format '%s' (argument -c) is not valid", arg_parser.settings.container_format); } else { gsr_log(GSR_LOG_LEVEL_ERROR, "Failed to deduce container format from file extension. Use the '-c' option to specify container format"); args_parser_print_usage(); @@ -3846,20 +3218,20 @@ int main(int argc, char **argv) { } if(file_extension.empty()) - file_extension = arg_parser.container_format ? arg_parser.container_format : ""; + file_extension = arg_parser.settings.container_format ? arg_parser.settings.container_format : ""; - const bool force_no_audio_offset = arg_parser.is_livestream || arg_parser.is_output_piped || (file_extension != "mp4" && file_extension != "mkv" && file_extension != "webm"); - const double target_fps = 1.0 / (double)arg_parser.fps; + const bool force_no_audio_offset = arg_parser.settings.is_livestream || arg_parser.settings.is_output_piped || (file_extension != "mp4" && file_extension != "mkv" && file_extension != "webm"); + const double target_fps = 1.0 / (double)arg_parser.settings.fps; const bool uses_amix = merged_audio_inputs_should_use_amix(requested_audio_inputs); - arg_parser.audio_codec = select_audio_codec_with_fallback(arg_parser.audio_codec, file_extension, uses_amix); + arg_parser.settings.audio_codec = select_audio_codec_with_fallback(arg_parser.settings.audio_codec, file_extension, uses_amix); vec2i video_size = {0, 0}; std::vector video_sources = create_video_sources(arg_parser, &egl, false, capture_sources, video_size); // (Some?) livestreaming services require at least one audio track to work. // If not audio is provided then create one silent audio track. - if(arg_parser.is_livestream && requested_audio_inputs.empty()) { + if(arg_parser.settings.is_livestream && requested_audio_inputs.empty()) { gsr_log(GSR_LOG_LEVEL_INFO, "live streaming but no audio track was added. Adding a silent audio track"); MergedAudioInputs mai; mai.audio_inputs.push_back({""}); @@ -3869,7 +3241,7 @@ int main(int argc, char **argv) { AVStream *video_stream = nullptr; std::vector audio_tracks; - if(arg_parser.video_encoder == GSR_VIDEO_ENCODER_HW_CPU && arg_parser.video_codec != (gsr_video_codec)GSR_VIDEO_CODEC_AUTO && arg_parser.video_codec != GSR_VIDEO_CODEC_H264) { + if(arg_parser.settings.video_encoder == GSR_VIDEO_ENCODER_HW_CPU && arg_parser.settings.video_codec != (gsr_video_codec)GSR_VIDEO_CODEC_AUTO && arg_parser.settings.video_codec != GSR_VIDEO_CODEC_H264) { gsr_log(GSR_LOG_LEVEL_ERROR, "-encoder cpu was specified but a codec other than h264 was specified. -encoder cpu supports only h264 at the moment"); _exit(1); } @@ -3877,9 +3249,9 @@ int main(int argc, char **argv) { bool low_power = false; const AVCodec *video_codec_f = select_video_codec_with_fallback(video_size, &arg_parser, file_extension.c_str(), &egl, &low_power); - const enum AVPixelFormat video_pix_fmt = get_pixel_format(arg_parser.video_codec, egl.gpu_info.vendor, arg_parser.video_encoder == GSR_VIDEO_ENCODER_HW_CPU); - AVCodecContext *video_codec_context = create_video_codec_context(video_pix_fmt, video_codec_f, egl, arg_parser, video_size.x, video_size.y); - if(!arg_parser.is_replaying) + const enum AVPixelFormat video_pix_fmt = get_pixel_format(arg_parser.settings.video_codec, egl.gpu_info.vendor, arg_parser.settings.video_encoder == GSR_VIDEO_ENCODER_HW_CPU); + AVCodecContext *video_codec_context = create_video_codec_context(video_pix_fmt, video_codec_f, &egl, &arg_parser.settings, video_size.x, video_size.y); + if(!arg_parser.settings.is_replaying) video_stream = create_stream(av_format_context, video_codec_context); AVFrame *video_frame = av_frame_alloc(); @@ -3896,9 +3268,9 @@ int main(int argc, char **argv) { video_frame->colorspace = video_codec_context->colorspace; video_frame->chroma_location = video_codec_context->chroma_sample_location; - const size_t estimated_replay_buffer_packets = calculate_estimated_replay_buffer_packets(arg_parser.replay_buffer_size_secs, arg_parser.fps, arg_parser.audio_codec, requested_audio_inputs); + const size_t estimated_replay_buffer_packets = calculate_estimated_replay_buffer_packets(arg_parser.settings.replay_buffer_size_secs, arg_parser.settings.fps, arg_parser.settings.audio_codec, requested_audio_inputs); gsr_encoder encoder; - if(!gsr_encoder_init(&encoder, arg_parser.replay_storage, estimated_replay_buffer_packets, arg_parser.replay_buffer_size_secs, arg_parser.filename)) { + if(!gsr_encoder_init(&encoder, arg_parser.settings.replay_storage, estimated_replay_buffer_packets, arg_parser.settings.replay_buffer_size_secs, arg_parser.settings.filename)) { gsr_log(GSR_LOG_LEVEL_ERROR, "failed to create encoder"); _exit(1); } @@ -3929,7 +3301,7 @@ int main(int argc, char **argv) { gsr_color_conversion_params color_conversion_params; memset(&color_conversion_params, 0, sizeof(color_conversion_params)); - color_conversion_params.color_range = arg_parser.color_range; + color_conversion_params.color_range = arg_parser.settings.color_range; color_conversion_params.egl = &egl; color_conversion_params.load_external_image_shader = any_video_sources_uses_external_image(video_sources); gsr_video_encoder_get_textures(video_encoder, color_conversion_params.destination_textures, color_conversion_params.destination_textures_size, &color_conversion_params.num_destination_textures, &color_conversion_params.destination_color); @@ -3944,17 +3316,19 @@ int main(int argc, char **argv) { gsr_color_conversion *output_color_conversion = plugins.num_plugins > 0 ? &plugins.color_conversion : &color_conversion; - if(arg_parser.video_encoder == GSR_VIDEO_ENCODER_HW_CPU) { - open_video_software(video_codec_context, arg_parser); + if(arg_parser.settings.video_encoder == GSR_VIDEO_ENCODER_HW_CPU) { + if(!open_video_software(video_codec_context, &arg_parser.settings)) + _exit(1); } else { - open_video_hardware(video_codec_context, low_power, egl, arg_parser); + if(!open_video_hardware(video_codec_context, low_power, &egl, &arg_parser.settings)) + _exit(1); } if(video_stream) { avcodec_parameters_from_context(video_stream->codecpar, video_codec_context); const size_t video_destination_id = gsr_encoder_add_recording_destination(&encoder, video_codec_context, av_format_context, video_stream, 0); - if(arg_parser.write_first_frame_ts && video_destination_id != (size_t)-1) { - std::string ts_filepath = std::string(arg_parser.filename) + ".ts"; + if(arg_parser.settings.write_first_frame_ts && video_destination_id != (size_t)-1) { + std::string ts_filepath = std::string(arg_parser.settings.filename) + ".ts"; gsr_encoder_set_recording_destination_first_frame_ts_filepath(&encoder, video_destination_id, ts_filepath.c_str()); } } @@ -3963,19 +3337,22 @@ int main(int argc, char **argv) { int audio_stream_index = VIDEO_STREAM_INDEX + 1; for(const MergedAudioInputs &merged_audio_inputs : requested_audio_inputs) { const bool use_amix = audio_inputs_should_use_amix(merged_audio_inputs.audio_inputs); - AVCodecContext *audio_codec_context = create_audio_codec_context(arg_parser.fps, arg_parser.audio_codec, use_amix, arg_parser.audio_bitrate); + AVCodecContext *audio_codec_context = create_audio_codec_context(arg_parser.settings.fps, arg_parser.settings.audio_codec, use_amix, arg_parser.settings.audio_bitrate); + if(!audio_codec_context) + _exit(1); AVStream *audio_stream = nullptr; - if(!arg_parser.is_replaying) { + if(!arg_parser.settings.is_replaying) { audio_stream = create_stream(av_format_context, audio_codec_context); if(gsr_encoder_add_recording_destination(&encoder, audio_codec_context, av_format_context, audio_stream, 0) == (size_t)-1) gsr_log(GSR_LOG_LEVEL_ERROR, "added too many audio sources"); } - if(audio_stream && !merged_audio_inputs.track_name.empty() && !arg_parser.exclude_metadata) + if(audio_stream && !merged_audio_inputs.track_name.empty() && !arg_parser.settings.exclude_metadata) av_dict_set(&audio_stream->metadata, "title", merged_audio_inputs.track_name.c_str(), 0); - open_audio(audio_codec_context, arg_parser.ffmpeg_audio_opts); + if(!open_audio(audio_codec_context, arg_parser.settings.ffmpeg_audio_opts)) + _exit(1); if(audio_stream) avcodec_parameters_from_context(audio_stream->codecpar, audio_codec_context); @@ -4003,7 +3380,7 @@ int main(int argc, char **argv) { const double audio_fps = (double)audio_codec_context->sample_rate / (double)audio_codec_context->frame_size; const double timeout_sec = 1000.0 / audio_fps / 1000.0; - const double audio_startup_time_seconds = force_no_audio_offset ? 0 : audio_codec_get_desired_delay(arg_parser.audio_codec, arg_parser.fps);// * ((double)audio_codec_context->frame_size / 1024.0); + const double audio_startup_time_seconds = force_no_audio_offset ? 0 : audio_codec_get_desired_delay(arg_parser.settings.audio_codec, arg_parser.settings.fps);// * ((double)audio_codec_context->frame_size / 1024.0); const double num_audio_frames_shift = audio_startup_time_seconds / timeout_sec; std::vector audio_track_audio_devices; @@ -4032,16 +3409,16 @@ int main(int argc, char **argv) { //av_dump_format(av_format_context, 0, filename, 1); - if(!arg_parser.is_replaying && !(output_format->flags & AVFMT_NOFILE)) { - const int ret = avio_open(&av_format_context->pb, arg_parser.filename, AVIO_FLAG_WRITE); + if(!arg_parser.settings.is_replaying && !(output_format->flags & AVFMT_NOFILE)) { + const int ret = avio_open(&av_format_context->pb, arg_parser.settings.filename, AVIO_FLAG_WRITE); if(ret < 0) { - gsr_log(GSR_LOG_LEVEL_ERROR, "Could not open '%s': %s", arg_parser.filename, av_error_to_string(ret)); + gsr_log(GSR_LOG_LEVEL_ERROR, "Could not open '%s': %s", arg_parser.settings.filename, gsr_av_error_to_string(ret)); _exit(1); } } - if(!arg_parser.is_replaying) - av_write_header(av_format_context, arg_parser.ffmpeg_opts); + if(!arg_parser.settings.is_replaying) + av_write_header(av_format_context, arg_parser.settings.ffmpeg_opts); double fps_start_time = clock_get_monotonic_seconds(); //double frame_timer_start = fps_start_time; @@ -4282,14 +3659,14 @@ int main(int argc, char **argv) { int64_t video_prev_pts = 0; bool hdr_metadata_set = false; - const bool hdr = video_codec_is_hdr(arg_parser.video_codec); + const bool hdr = video_codec_is_hdr(arg_parser.settings.video_codec); bool use_damage_tracking = false; gsr_damage damage; memset(&damage, 0, sizeof(damage)); - if(arg_parser.framerate_mode == GSR_FRAMERATE_MODE_CONTENT && is_capturing_damage_tracked_target(capture_sources)) { + if(arg_parser.settings.framerate_mode == GSR_FRAMERATE_MODE_CONTENT && is_capturing_damage_tracked_target(capture_sources)) { if(gsr_window_get_display_server(window) == GSR_DISPLAY_SERVER_X11) { - gsr_damage_init(&damage, &egl, &x11_cursor, arg_parser.record_cursor); + gsr_damage_init(&damage, &egl, &x11_cursor, arg_parser.settings.record_cursor); use_damage_tracking = true; for(const CaptureSource &capture_source : capture_sources) { @@ -4313,7 +3690,7 @@ int main(int argc, char **argv) { while(running) { while(gsr_window_process_event(window)) { - if(x11_cursor_display && arg_parser.record_cursor) + if(x11_cursor_display && arg_parser.settings.record_cursor) gsr_cursor_on_event(&x11_cursor, gsr_window_get_event_data(window)); gsr_damage_on_event(&damage, gsr_window_get_event_data(window)); @@ -4322,7 +3699,7 @@ int main(int argc, char **argv) { } } - if(x11_cursor_display && arg_parser.record_cursor) + if(x11_cursor_display && arg_parser.settings.record_cursor) gsr_cursor_tick(&x11_cursor, DefaultRootWindow(x11_cursor_display)); gsr_damage_tick(&damage); @@ -4363,7 +3740,7 @@ int main(int argc, char **argv) { damaged |= gsr_plugins_is_damaged(&plugins); // TODO: Readd wayland sync warning when removing this - if(arg_parser.framerate_mode != GSR_FRAMERATE_MODE_CONTENT) + if(arg_parser.settings.framerate_mode != GSR_FRAMERATE_MODE_CONTENT) damaged = true; if(damaged) @@ -4374,7 +3751,7 @@ int main(int argc, char **argv) { //const double frame_timer_elapsed = time_now - frame_timer_start; const double elapsed = time_now - fps_start_time; if (elapsed >= 1.0) { - if(arg_parser.verbose) { + if(arg_parser.settings.verbose) { fprintf(stderr, "update fps: %d, damage fps: %d\n", fps_counter, damage_fps_counter); } fps_start_time = time_now; @@ -4446,14 +3823,14 @@ int main(int argc, char **argv) { gsr_video_encoder_copy_textures_to_frame(video_encoder, video_frame, output_color_conversion); for(VideoSource &video_source : video_sources) { - if(hdr && !hdr_metadata_set && !arg_parser.is_replaying && add_hdr_metadata_to_video_stream(video_source.capture, video_stream)) + if(hdr && !hdr_metadata_set && !arg_parser.settings.is_replaying && add_hdr_metadata_to_video_stream(video_source.capture, video_stream)) hdr_metadata_set = true; } // TODO: Check if duplicate frame can be saved just by writing it with a different pts instead of sending it again - const int num_frames_to_encode = arg_parser.framerate_mode == GSR_FRAMERATE_MODE_CONSTANT ? num_missed_frames : 1; + const int num_frames_to_encode = arg_parser.settings.framerate_mode == GSR_FRAMERATE_MODE_CONSTANT ? num_missed_frames : 1; for(int i = 0; i < num_frames_to_encode; ++i) { - if(arg_parser.framerate_mode == GSR_FRAMERATE_MODE_CONSTANT) { + if(arg_parser.settings.framerate_mode == GSR_FRAMERATE_MODE_CONSTANT) { video_frame->pts = video_pts_counter + i; } else { video_frame->pts = (this_video_frame_time - record_start_time) * (double)AV_TIME_BASE; @@ -4472,7 +3849,7 @@ int main(int argc, char **argv) { // TODO: Move to separate thread because this could write to network (for example when livestreaming) gsr_encoder_receive_packets(&encoder, video_codec_context, video_frame->pts, VIDEO_STREAM_INDEX); } else { - gsr_log(GSR_LOG_LEVEL_ERROR, "avcodec_send_frame failed, error: %s", av_error_to_string(ret)); + gsr_log(GSR_LOG_LEVEL_ERROR, "avcodec_send_frame failed, error: %s", gsr_av_error_to_string(ret)); } if(force_iframe_frame) { @@ -4484,7 +3861,7 @@ int main(int argc, char **argv) { video_pts_counter += num_missed_frames; } - if(toggle_pause == 1 && !arg_parser.is_replaying) { + if(toggle_pause == 1 && !arg_parser.settings.is_replaying) { const bool new_paused_state = !paused; if(new_paused_state) { paused_time_start = clock_get_monotonic_seconds(); @@ -4498,23 +3875,23 @@ int main(int argc, char **argv) { paused = !paused; } - if(toggle_replay_recording && !arg_parser.replay_recording_directory) { + if(toggle_replay_recording && !arg_parser.settings.replay_recording_directory) { toggle_replay_recording = 0; printf("gsr error: Unable to start recording since the -ro option was not specified\n"); fflush(stdout); } - if(toggle_replay_recording && arg_parser.replay_recording_directory) { + if(toggle_replay_recording && arg_parser.settings.replay_recording_directory) { toggle_replay_recording = 0; const bool new_replay_recording_state = !replay_recording; if(new_replay_recording_state) { std::lock_guard lock(audio_filter_mutex); replay_recording_items.clear(); - replay_recording_filepath = create_new_recording_filepath_from_timestamp(arg_parser.replay_recording_directory, "Video", file_extension, arg_parser.date_folders); + replay_recording_filepath = create_new_recording_filepath_from_timestamp(arg_parser.settings.replay_recording_directory, "Video", file_extension, arg_parser.settings.date_folders); replay_recording_start_result = start_recording_create_streams(replay_recording_filepath.c_str(), arg_parser, video_codec_context, audio_tracks, hdr, video_sources); if(replay_recording_start_result.av_format_context) { const size_t video_recording_destination_id = gsr_encoder_add_recording_destination(&encoder, video_codec_context, replay_recording_start_result.av_format_context, replay_recording_start_result.video_stream, video_frame->pts); - if(arg_parser.write_first_frame_ts && video_recording_destination_id != (size_t)-1) { + if(arg_parser.settings.write_first_frame_ts && video_recording_destination_id != (size_t)-1) { std::string ts_filepath = replay_recording_filepath + ".ts"; gsr_encoder_set_recording_destination_first_frame_ts_filepath(&encoder, video_recording_destination_id, ts_filepath.c_str()); } @@ -4545,8 +3922,8 @@ int main(int argc, char **argv) { fprintf(stderr, "Stopped recording\n"); puts(replay_recording_filepath.c_str()); fflush(stdout); - if(arg_parser.recording_saved_script) - run_recording_saved_script_async(arg_parser.recording_saved_script, replay_recording_filepath.c_str(), "regular"); + if(arg_parser.settings.recording_saved_script) + run_recording_saved_script_async(arg_parser.settings.recording_saved_script, replay_recording_filepath.c_str(), "regular"); } else { printf("gsr error: Failed to save recording\n"); fflush(stdout); @@ -4566,25 +3943,25 @@ int main(int argc, char **argv) { } else { puts(save_replay_output_filepath.c_str()); fflush(stdout); - if(arg_parser.recording_saved_script) - run_recording_saved_script_async(arg_parser.recording_saved_script, save_replay_output_filepath.c_str(), "replay"); + if(arg_parser.settings.recording_saved_script) + run_recording_saved_script_async(arg_parser.settings.recording_saved_script, save_replay_output_filepath.c_str(), "replay"); } } - if(save_replay_seconds != 0 && !save_replay_thread.valid() && arg_parser.is_replaying) { + if(save_replay_seconds != 0 && !save_replay_thread.valid() && arg_parser.settings.is_replaying) { int current_save_replay_seconds = save_replay_seconds; if(current_save_replay_seconds > 0) - current_save_replay_seconds += arg_parser.keyint; + current_save_replay_seconds += arg_parser.settings.keyint; save_replay_seconds = 0; save_replay_output_filepath.clear(); - const bool replay_start_result = save_replay_async(video_codec_context, VIDEO_STREAM_INDEX, audio_tracks, &encoder, arg_parser, file_extension, arg_parser.date_folders, hdr, video_sources, current_save_replay_seconds); + const bool replay_start_result = save_replay_async(video_codec_context, VIDEO_STREAM_INDEX, audio_tracks, &encoder, arg_parser, file_extension, arg_parser.settings.date_folders, hdr, video_sources, current_save_replay_seconds); if(!replay_start_result) { printf("gsr error: Failed to save replay\n"); fflush(stdout); } - if(arg_parser.restart_replay_on_save && current_save_replay_seconds == save_replay_seconds_full) { + if(arg_parser.settings.restart_replay_on_save && current_save_replay_seconds == save_replay_seconds_full) { pthread_mutex_lock(&encoder.replay_mutex); gsr_replay_buffer_clear(encoder.replay_buffer); pthread_mutex_unlock(&encoder.replay_mutex); @@ -4605,7 +3982,7 @@ int main(int argc, char **argv) { else { if(paused) av_usleep(20.0 * 1000.0); // 20 milliseconds - else if(arg_parser.framerate_mode == GSR_FRAMERATE_MODE_CONTENT) + else if(arg_parser.settings.framerate_mode == GSR_FRAMERATE_MODE_CONTENT) av_usleep(2.8 * 1000.0); // 2.8 milliseconds } } @@ -4619,8 +3996,8 @@ int main(int argc, char **argv) { } else { puts(save_replay_output_filepath.c_str()); fflush(stdout); - if(arg_parser.recording_saved_script) - run_recording_saved_script_async(arg_parser.recording_saved_script, save_replay_output_filepath.c_str(), "replay"); + if(arg_parser.settings.recording_saved_script) + run_recording_saved_script_async(arg_parser.settings.recording_saved_script, save_replay_output_filepath.c_str(), "replay"); } } @@ -4636,8 +4013,8 @@ int main(int argc, char **argv) { fprintf(stderr, "Stopped recording\n"); puts(replay_recording_filepath.c_str()); fflush(stdout); - if(arg_parser.recording_saved_script) - run_recording_saved_script_async(arg_parser.recording_saved_script, replay_recording_filepath.c_str(), "regular"); + if(arg_parser.settings.recording_saved_script) + run_recording_saved_script_async(arg_parser.settings.recording_saved_script, replay_recording_filepath.c_str(), "regular"); } else { printf("gsr error: Failed to save recording\n"); fflush(stdout); @@ -4655,11 +4032,11 @@ int main(int argc, char **argv) { amix_thread.join(); // TODO: Replace this with start_recording_create_steams - if(!arg_parser.is_replaying && av_write_trailer(av_format_context) != 0) { + if(!arg_parser.settings.is_replaying && av_write_trailer(av_format_context) != 0) { //fprintf(stderr, "Failed to write trailer\n"); } - if(!arg_parser.is_replaying && !(output_format->flags & AVFMT_NOFILE)) { + if(!arg_parser.settings.is_replaying && !(output_format->flags & AVFMT_NOFILE)) { avio_close(av_format_context->pb); avformat_free_context(av_format_context); } @@ -4682,8 +4059,8 @@ int main(int argc, char **argv) { gsr_kde_night_light_destroy(kde_night_light); - if(!arg_parser.is_replaying && arg_parser.recording_saved_script) - run_recording_saved_script_async(arg_parser.recording_saved_script, arg_parser.filename, "regular"); + if(!arg_parser.settings.is_replaying && arg_parser.settings.recording_saved_script) + run_recording_saved_script_async(arg_parser.settings.recording_saved_script, arg_parser.settings.filename, "regular"); if(dpy) { // TODO: This causes a crash, why? maybe some other library dlclose xlib and that also happened to unload this??? diff --git a/src/recorder/audio_codec.c b/src/recorder/audio_codec.c new file mode 100644 index 0000000..1b2d6db --- /dev/null +++ b/src/recorder/audio_codec.c @@ -0,0 +1,220 @@ +#include "../../include/recorder/audio_codec.h" +#include "../../include/ffmpeg_utils.h" +#include "../../include/log.h" + +#include +#include +#include + +#include + +enum AVCodecID audio_codec_get_id(gsr_audio_codec audio_codec) { + switch(audio_codec) { + case GSR_AUDIO_CODEC_AAC: return AV_CODEC_ID_AAC; + case GSR_AUDIO_CODEC_OPUS: return AV_CODEC_ID_OPUS; + case GSR_AUDIO_CODEC_FLAC: return AV_CODEC_ID_FLAC; + } + assert(false); + return AV_CODEC_ID_AAC; +} + +enum AVSampleFormat audio_codec_get_sample_format(AVCodecContext *audio_codec_context, gsr_audio_codec audio_codec, const AVCodec *codec, bool mix_audio) { + (void)audio_codec_context; + switch(audio_codec) { + case GSR_AUDIO_CODEC_AAC: { + return AV_SAMPLE_FMT_FLTP; + } + case GSR_AUDIO_CODEC_OPUS: { + bool supports_s16 = false; + bool supports_flt = false; + + #if LIBAVCODEC_VERSION_INT < AV_VERSION_INT(61, 15, 0) + for(size_t i = 0; codec->sample_fmts && codec->sample_fmts[i] != AV_SAMPLE_FMT_NONE; ++i) { + if(codec->sample_fmts[i] == AV_SAMPLE_FMT_S16) { + supports_s16 = true; + } else if(codec->sample_fmts[i] == AV_SAMPLE_FMT_FLT) { + supports_flt = true; + } + } + #else + const enum AVSampleFormat *sample_fmts = NULL; + if(avcodec_get_supported_config(audio_codec_context, codec, AV_CODEC_CONFIG_SAMPLE_FORMAT, 0, (const void**)&sample_fmts, NULL) >= 0) { + if(sample_fmts) { + for(size_t i = 0; sample_fmts[i] != AV_SAMPLE_FMT_NONE; ++i) { + if(sample_fmts[i] == AV_SAMPLE_FMT_S16) { + supports_s16 = true; + } else if(sample_fmts[i] == AV_SAMPLE_FMT_FLT) { + supports_flt = true; + } + } + } else { + // What a dumb API. It returns NULL if all formats are supported + supports_s16 = true; + supports_flt = true; + } + } + #endif + + // Amix only works with float audio + if(mix_audio) + supports_s16 = false; + + if(!supports_s16 && !supports_flt) { + gsr_log(GSR_LOG_LEVEL_WARNING, "opus audio codec is chosen but your ffmpeg version does not support s16/flt sample format and performance might be slightly worse.\n" + " You can either rebuild ffmpeg with libopus instead of the built-in opus, use the flatpak version of gpu screen recorder or record with aac audio codec instead (-ac aac).\n" + " Falling back to fltp audio sample format instead."); + } + + if(supports_s16) + return AV_SAMPLE_FMT_S16; + else if(supports_flt) + return AV_SAMPLE_FMT_FLT; + else + return AV_SAMPLE_FMT_FLTP; + } + case GSR_AUDIO_CODEC_FLAC: { + return AV_SAMPLE_FMT_S32; + } + } + assert(false); + return AV_SAMPLE_FMT_FLTP; +} + +int64_t audio_codec_get_get_bitrate(gsr_audio_codec audio_codec) { + switch(audio_codec) { + case GSR_AUDIO_CODEC_AAC: return 160000; + case GSR_AUDIO_CODEC_OPUS: return 128000; + case GSR_AUDIO_CODEC_FLAC: return 128000; + } + assert(false); + return 128000; +} + +gsr_audio_format audio_codec_context_get_audio_format(const AVCodecContext *audio_codec_context) { + switch(audio_codec_context->sample_fmt) { + case AV_SAMPLE_FMT_FLT: return GSR_AUDIO_FORMAT_F32; + case AV_SAMPLE_FMT_FLTP: return GSR_AUDIO_FORMAT_S32; + case AV_SAMPLE_FMT_S16: return GSR_AUDIO_FORMAT_S16; + case AV_SAMPLE_FMT_S32: return GSR_AUDIO_FORMAT_S32; + default: return GSR_AUDIO_FORMAT_S16; + } +} + +enum AVSampleFormat audio_format_to_sample_format(const gsr_audio_format audio_format) { + switch(audio_format) { + case GSR_AUDIO_FORMAT_S16: return AV_SAMPLE_FMT_S16; + case GSR_AUDIO_FORMAT_S32: return AV_SAMPLE_FMT_S32; + case GSR_AUDIO_FORMAT_F32: return AV_SAMPLE_FMT_FLT; + } + assert(false); + return AV_SAMPLE_FMT_S16; +} + +AVCodecContext* create_audio_codec_context(int fps, gsr_audio_codec audio_codec, bool mix_audio, int64_t audio_bitrate) { + (void)fps; + const AVCodec *codec = avcodec_find_encoder(audio_codec_get_id(audio_codec)); + if (!codec) { + gsr_log(GSR_LOG_LEVEL_ERROR, "Could not find %s audio encoder", audio_codec_get_name(audio_codec)); + return NULL; + } + + AVCodecContext *codec_context = avcodec_alloc_context3(codec); + + assert(codec->type == AVMEDIA_TYPE_AUDIO); + codec_context->codec_id = codec->id; + codec_context->sample_fmt = audio_codec_get_sample_format(codec_context, audio_codec, codec, mix_audio); + codec_context->bit_rate = audio_bitrate == 0 ? audio_codec_get_get_bitrate(audio_codec) : audio_bitrate; + codec_context->sample_rate = GSR_AUDIO_SAMPLE_RATE; + if(audio_codec == GSR_AUDIO_CODEC_AAC) { +#if LIBAVCODEC_VERSION_MAJOR < 62 + codec_context->profile = FF_PROFILE_AAC_LOW; +#else + codec_context->profile = AV_PROFILE_AAC_LOW; +#endif + } +#if LIBAVCODEC_VERSION_MAJOR < 60 + codec_context->channel_layout = AV_CH_LAYOUT_STEREO; + codec_context->channels = 2; +#else + av_channel_layout_default(&codec_context->ch_layout, 2); +#endif + + codec_context->time_base.num = 1; + codec_context->time_base.den = codec_context->sample_rate; + codec_context->thread_count = 1; + codec_context->flags |= AV_CODEC_FLAG_GLOBAL_HEADER; + + return codec_context; +} + +bool open_audio(AVCodecContext *audio_codec_context, const char *ffmpeg_audio_opts) { + AVDictionary *options = NULL; + av_dict_set(&options, "strict", "experimental", 0); + + if(ffmpeg_audio_opts) + av_dict_parse_string(&options, ffmpeg_audio_opts, "=", ";", 0); + + int ret; + ret = avcodec_open2(audio_codec_context, audio_codec_context->codec, &options); + if(ret < 0) { + gsr_log(GSR_LOG_LEVEL_ERROR, "failed to open audio codec, reason: %s", gsr_av_error_to_string(ret)); + return false; + } + + return true; +} + +AVFrame* create_audio_frame(AVCodecContext *audio_codec_context) { + AVFrame *frame = av_frame_alloc(); + if(!frame) { + gsr_log(GSR_LOG_LEVEL_ERROR, "failed to allocate audio frame"); + return NULL; + } + + frame->sample_rate = audio_codec_context->sample_rate; + frame->nb_samples = audio_codec_context->frame_size; + frame->format = audio_codec_context->sample_fmt; +#if LIBAVCODEC_VERSION_MAJOR < 60 + frame->channels = audio_codec_context->channels; + frame->channel_layout = audio_codec_context->channel_layout; +#else + av_channel_layout_copy(&frame->ch_layout, &audio_codec_context->ch_layout); +#endif + + int ret = av_frame_get_buffer(frame, 0); + if(ret < 0) { + gsr_log(GSR_LOG_LEVEL_ERROR, "failed to allocate audio data buffers, reason: %s", gsr_av_error_to_string(ret)); + av_frame_free(&frame); + return NULL; + } + + return frame; +} + +double audio_codec_get_desired_delay(gsr_audio_codec audio_codec, int fps) { + const double fps_inv = 1.0 / (double)fps; + const double base = 0.01 + 1.0/165.0; + switch(audio_codec) { + case GSR_AUDIO_CODEC_OPUS: + return fmax(0.0, base - fps_inv); + case GSR_AUDIO_CODEC_AAC: + return fmax(0.0, (base + 0.008) * 2.0 - fps_inv); + case GSR_AUDIO_CODEC_FLAC: + // TODO: Test + return fmax(0.0, base - fps_inv); + } + assert(false); + return fmax(0.0, base - fps_inv); +} + +int audio_codec_get_frame_size(gsr_audio_codec audio_codec) { + switch(audio_codec) { + case GSR_AUDIO_CODEC_AAC: return 1024; + case GSR_AUDIO_CODEC_OPUS: return 960; + case GSR_AUDIO_CODEC_FLAC: + assert(false); + return 1024; + } + assert(false); + return 1024; +} diff --git a/src/recorder/video_codec.c b/src/recorder/video_codec.c new file mode 100644 index 0000000..1713e77 --- /dev/null +++ b/src/recorder/video_codec.c @@ -0,0 +1,429 @@ +#include "../../include/recorder/video_codec.h" +#include "../../include/ffmpeg_utils.h" +#include "../../include/log.h" + +#include +#include + +#include + +int video_quality_to_h264_equivalent_qp(gsr_video_quality video_quality) { + switch(video_quality) { + case GSR_VIDEO_QUALITY_MEDIUM: return 35; + case GSR_VIDEO_QUALITY_HIGH: return 30; + case GSR_VIDEO_QUALITY_VERY_HIGH: return 25; + case GSR_VIDEO_QUALITY_ULTRA: return 22; + } + return 22; +} + +static int video_quality_to_codec_quality_value(enum AVCodecID codec_id, gsr_video_quality video_quality) { + const int h264_qp = video_quality_to_h264_equivalent_qp(video_quality); + switch(codec_id) { + case AV_CODEC_ID_H264: + case AV_CODEC_ID_HEVC: + return h264_qp; + case AV_CODEC_ID_AV1: + case AV_CODEC_ID_VP9: + return h264_qp * 4; + case AV_CODEC_ID_VP8: + return h264_qp * 2; + default: + return h264_qp; + } +} + +static int vbr_get_quality_parameter(AVCodecContext *codec_context, gsr_video_quality video_quality, bool hdr) { + // 8 bit / 10 bit = 80% + const float qp_multiply = hdr ? 8.0f/10.0f : 1.0f; + return video_quality_to_codec_quality_value(codec_context->codec_id, video_quality) * qp_multiply; +} + +AVCodecContext *create_video_codec_context(enum AVPixelFormat pix_fmt, const AVCodec *codec, const gsr_egl *egl, const gsr_recorder_settings *settings, int width, int height) { + const bool use_software_video_encoder = settings->video_encoder == GSR_VIDEO_ENCODER_HW_CPU; + const bool hdr = video_codec_is_hdr(settings->video_codec); + AVCodecContext *codec_context = avcodec_alloc_context3(codec); + + //double fps_ratio = (double)fps / 30.0; + + assert(codec->type == AVMEDIA_TYPE_VIDEO); + codec_context->codec_id = codec->id; + codec_context->width = width; + codec_context->height = height; + // Timebase: This is the fundamental unit of time (in seconds) in terms + // of which frame timestamps are represented. For fixed-fps content, + // timebase should be 1/framerate and timestamp increments should be + // identical to 1 + codec_context->time_base.num = 1; + codec_context->time_base.den = settings->framerate_mode == GSR_FRAMERATE_MODE_CONSTANT ? settings->fps : AV_TIME_BASE; + codec_context->framerate.num = settings->fps; + codec_context->framerate.den = 1; + codec_context->sample_aspect_ratio.num = 0; + codec_context->sample_aspect_ratio.den = 0; + if(settings->low_latency_recording) { + codec_context->flags |= (AV_CODEC_FLAG_CLOSED_GOP | AV_CODEC_FLAG_LOW_DELAY); + codec_context->flags2 |= AV_CODEC_FLAG2_FAST; + //codec_context->gop_size = std::numeric_limits::max(); + //codec_context->keyint_min = std::numeric_limits::max(); + codec_context->gop_size = settings->fps * settings->keyint; + } else { + // High values reduce file size but increases time it takes to seek + codec_context->gop_size = settings->fps * settings->keyint; + } + codec_context->max_b_frames = 0; + codec_context->pix_fmt = pix_fmt; + codec_context->color_range = settings->color_range == GSR_COLOR_RANGE_LIMITED ? AVCOL_RANGE_MPEG : AVCOL_RANGE_JPEG; + if(hdr) { + codec_context->color_primaries = AVCOL_PRI_BT2020; + codec_context->color_trc = AVCOL_TRC_SMPTE2084; + codec_context->colorspace = AVCOL_SPC_BT2020_NCL; + } else { + codec_context->color_primaries = AVCOL_PRI_BT709; + codec_context->color_trc = AVCOL_TRC_BT709; + codec_context->colorspace = AVCOL_SPC_BT709; + } + //codec_context->chroma_sample_location = AVCHROMA_LOC_CENTER; + // Can't use this because it's fucking broken in ffmpeg 8 or new mesa. It produces garbage output + //if(codec->id == AV_CODEC_ID_HEVC) + // codec_context->codec_tag = MKTAG('h', 'v', 'c', '1'); // QuickTime on MacOS requires this or the video wont be playable + + if(settings->bitrate_mode == GSR_BITRATE_MODE_CBR) { + codec_context->bit_rate = settings->video_bitrate; + codec_context->rc_max_rate = codec_context->bit_rate; + //codec_context->rc_min_rate = codec_context->bit_rate; + codec_context->rc_buffer_size = codec_context->bit_rate;//codec_context->bit_rate / 10; + codec_context->rc_initial_buffer_occupancy = 0;//codec_context->bit_rate;//codec_context->bit_rate * 1000; + } else if(settings->bitrate_mode == GSR_BITRATE_MODE_VBR) { + const int quality = vbr_get_quality_parameter(codec_context, settings->video_quality, hdr); + switch(settings->video_quality) { + case GSR_VIDEO_QUALITY_MEDIUM: + codec_context->qmin = quality; + codec_context->qmax = quality; + codec_context->bit_rate = 100000;//4500000 + (codec_context->width * codec_context->height)*0.75; + break; + case GSR_VIDEO_QUALITY_HIGH: + codec_context->qmin = quality; + codec_context->qmax = quality; + codec_context->bit_rate = 100000;//10000000-9000000 + (codec_context->width * codec_context->height)*0.75; + break; + case GSR_VIDEO_QUALITY_VERY_HIGH: + codec_context->qmin = quality; + codec_context->qmax = quality; + codec_context->bit_rate = 100000;//10000000-9000000 + (codec_context->width * codec_context->height)*0.75; + break; + case GSR_VIDEO_QUALITY_ULTRA: + codec_context->qmin = quality; + codec_context->qmax = quality; + codec_context->bit_rate = 100000;//10000000-9000000 + (codec_context->width * codec_context->height)*0.75; + break; + } + + codec_context->rc_max_rate = codec_context->bit_rate; + //codec_context->rc_min_rate = codec_context->bit_rate; + codec_context->rc_buffer_size = codec_context->bit_rate;//codec_context->bit_rate / 10; + codec_context->rc_initial_buffer_occupancy = codec_context->bit_rate;//codec_context->bit_rate * 1000; + } else { + //codec_context->rc_buffer_size = 50000 * 1000; + } + //codec_context->profile = FF_PROFILE_H264_MAIN; + if (codec_context->codec_id == AV_CODEC_ID_MPEG1VIDEO) + codec_context->mb_decision = 2; + + const bool uses_vaapi_encoder = !use_software_video_encoder && egl->gpu_info.vendor != GSR_GPU_VENDOR_NVIDIA && !video_codec_is_vulkan(settings->video_codec); + if(uses_vaapi_encoder && settings->bitrate_mode != GSR_BITRATE_MODE_CBR) { + // 8 bit / 10 bit = 80%, and increase it even more + const float quality_multiply = hdr ? (8.0f/10.0f * 0.7f) : 1.0f; + codec_context->global_quality = video_quality_to_codec_quality_value(codec_context->codec_id, settings->video_quality) * quality_multiply; + } + + av_opt_set_int(codec_context->priv_data, "b_ref_mode", 0, 0); + //av_opt_set_int(codec_context->priv_data, "cbr", true, 0); + + if(egl->gpu_info.vendor != GSR_GPU_VENDOR_NVIDIA || video_codec_is_vulkan(settings->video_codec)) { + // TODO: More options, better options + //codec_context->bit_rate = codec_context->width * codec_context->height; + switch(settings->bitrate_mode) { + case GSR_BITRATE_MODE_QP: { + if(video_codec_is_vulkan(settings->video_codec)) + av_opt_set(codec_context->priv_data, "rc_mode", "cqp", 0); + else if(egl->gpu_info.vendor == GSR_GPU_VENDOR_NVIDIA) + av_opt_set(codec_context->priv_data, "rc", "constqp", 0); + else + av_opt_set(codec_context->priv_data, "rc_mode", "CQP", 0); + break; + } + case GSR_BITRATE_MODE_VBR: { + if(video_codec_is_vulkan(settings->video_codec)) + av_opt_set(codec_context->priv_data, "rc_mode", "vbr", 0); + else if(egl->gpu_info.vendor == GSR_GPU_VENDOR_NVIDIA) + av_opt_set(codec_context->priv_data, "rc", "vbr", 0); + else + av_opt_set(codec_context->priv_data, "rc_mode", "VBR", 0); + break; + } + case GSR_BITRATE_MODE_CBR: { + if(video_codec_is_vulkan(settings->video_codec)) + av_opt_set(codec_context->priv_data, "rc_mode", "cbr", 0); + else if(egl->gpu_info.vendor == GSR_GPU_VENDOR_NVIDIA) + av_opt_set(codec_context->priv_data, "rc", "cbr", 0); + else + av_opt_set(codec_context->priv_data, "rc_mode", "CBR", 0); + break; + } + } + //codec_context->global_quality = 4; + //codec_context->compression_level = 2; + } + + //av_opt_set(codec_context->priv_data, "bsf", "hevc_metadata=colour_primaries=9:transfer_characteristics=16:matrix_coefficients=9", 0); + + if(settings->tune == GSR_TUNE_QUALITY) + codec_context->max_b_frames = 2; + + codec_context->flags |= AV_CODEC_FLAG_GLOBAL_HEADER; + + return codec_context; +} + +static void dict_set_profile(AVCodecContext *codec_context, gsr_gpu_vendor vendor, gsr_color_depth color_depth, gsr_video_codec video_codec, AVDictionary **options) { + #if LIBAVCODEC_VERSION_INT < AV_VERSION_INT(61, 17, 100) + if(codec_context->codec_id == AV_CODEC_ID_H264) { + // TODO: Only for vaapi + //if(color_depth == GSR_COLOR_DEPTH_10_BITS) + // av_dict_set(options, "profile", "high10", 0); + //else + av_dict_set(options, "profile", "high", 0); + } else if(codec_context->codec_id == AV_CODEC_ID_AV1) { + if(vendor == GSR_GPU_VENDOR_NVIDIA) { + if(color_depth == GSR_COLOR_DEPTH_10_BITS) + av_dict_set_int(options, "highbitdepth", 1, 0); + } else { + av_dict_set(options, "profile", "main", 0); // TODO: use professional instead? + } + } else if(codec_context->codec_id == AV_CODEC_ID_HEVC) { + if(color_depth == GSR_COLOR_DEPTH_10_BITS) + av_dict_set(options, "profile", "main10", 0); + else + av_dict_set(options, "profile", "main", 0); + } + #else + const bool use_nvidia_values = vendor == GSR_GPU_VENDOR_NVIDIA && !video_codec_is_vulkan(video_codec); + if(codec_context->codec_id == AV_CODEC_ID_H264) { + // TODO: Only for vaapi + //if(color_depth == GSR_COLOR_DEPTH_10_BITS) + // av_dict_set_int(options, "profile", AV_PROFILE_H264_HIGH_10, 0); + //else + av_dict_set_int(options, "profile", use_nvidia_values ? 2 : AV_PROFILE_H264_HIGH, 0); + } else if(codec_context->codec_id == AV_CODEC_ID_AV1) { + if(use_nvidia_values) { + if(color_depth == GSR_COLOR_DEPTH_10_BITS) + av_dict_set_int(options, "highbitdepth", 1, 0); + } else { + av_dict_set_int(options, "profile", AV_PROFILE_AV1_MAIN, 0); // TODO: use professional instead? + } + } else if(codec_context->codec_id == AV_CODEC_ID_HEVC) { + if(color_depth == GSR_COLOR_DEPTH_10_BITS) + av_dict_set_int(options, "profile", use_nvidia_values ? 1 : AV_PROFILE_HEVC_MAIN_10, 0); + else + av_dict_set_int(options, "profile", use_nvidia_values ? 0 : AV_PROFILE_HEVC_MAIN, 0); + } + #endif +} + +static void video_software_set_qp(AVCodecContext *codec_context, gsr_video_quality video_quality, bool hdr, AVDictionary **options) { + // 8 bit / 10 bit = 80% + const float qp_multiply = hdr ? 8.0f/10.0f : 1.0f; + av_dict_set_int(options, "qp", video_quality_to_codec_quality_value(codec_context->codec_id, video_quality) * qp_multiply, 0); +} + +bool open_video_software(AVCodecContext *codec_context, const gsr_recorder_settings *settings) { + const bool hdr = video_codec_is_hdr(settings->video_codec); + AVDictionary *options = NULL; + + if(settings->bitrate_mode == GSR_BITRATE_MODE_QP) + video_software_set_qp(codec_context, settings->video_quality, hdr, &options); + + av_dict_set(&options, "preset", "veryfast", 0); + av_dict_set(&options, "tune", "film", 0); + av_dict_set_int(&options, "forced-idr", 1, 0); + + if(codec_context->codec_id == AV_CODEC_ID_H264) { + av_dict_set(&options, "coder", "cabac", 0); // TODO: cavlc is faster than cabac but worse compression. Which to use? + } + + av_dict_set(&options, "strict", "experimental", 0); + + if(settings->ffmpeg_video_opts) + av_dict_parse_string(&options, settings->ffmpeg_video_opts, "=", ";", 0); + + int ret = avcodec_open2(codec_context, codec_context->codec, &options); + if (ret < 0) { + gsr_log(GSR_LOG_LEVEL_ERROR, "Could not open video codec: %s", gsr_av_error_to_string(ret)); + return false; + } + + return true; +} + +static void video_set_rc(gsr_video_codec video_codec, gsr_gpu_vendor vendor, gsr_bitrate_mode bitrate_mode, AVDictionary **options) { + switch(bitrate_mode) { + case GSR_BITRATE_MODE_QP: { + if(video_codec_is_vulkan(video_codec)) + av_dict_set(options, "rc_mode", "cqp", 0); + else if(vendor == GSR_GPU_VENDOR_NVIDIA) + av_dict_set(options, "rc", "constqp", 0); + else + av_dict_set(options, "rc_mode", "CQP", 0); + break; + } + case GSR_BITRATE_MODE_VBR: { + if(video_codec_is_vulkan(video_codec)) + av_dict_set(options, "rc_mode", "vbr", 0); + else if(vendor == GSR_GPU_VENDOR_NVIDIA) + av_dict_set(options, "rc", "vbr", 0); + else + av_dict_set(options, "rc_mode", "VBR", 0); + break; + } + case GSR_BITRATE_MODE_CBR: { + if(video_codec_is_vulkan(video_codec)) + av_dict_set(options, "rc_mode", "cbr", 0); + else if(vendor == GSR_GPU_VENDOR_NVIDIA) + av_dict_set(options, "rc", "cbr", 0); + else + av_dict_set(options, "rc_mode", "CBR", 0); + break; + } + } +} + +static void video_hardware_set_qp(AVCodecContext *codec_context, gsr_video_quality video_quality, bool hdr, AVDictionary **options) { + // 8 bit / 10 bit = 80% + const float qp_multiply = hdr ? 8.0f/10.0f : 1.0f; + av_dict_set_int(options, "qp", video_quality_to_codec_quality_value(codec_context->codec_id, video_quality) * qp_multiply, 0); +} + +bool open_video_hardware(AVCodecContext *codec_context, bool low_power, const gsr_egl *egl, const gsr_recorder_settings *settings) { + const gsr_color_depth color_depth = video_codec_to_bit_depth(settings->video_codec); + const bool hdr = video_codec_is_hdr(settings->video_codec); + AVDictionary *options = NULL; + + if(settings->bitrate_mode == GSR_BITRATE_MODE_QP) + video_hardware_set_qp(codec_context, settings->video_quality, hdr, &options); + + video_set_rc(settings->video_codec, egl->gpu_info.vendor, settings->bitrate_mode, &options); + + // TODO: Enable multipass + + dict_set_profile(codec_context, egl->gpu_info.vendor, color_depth, settings->video_codec, &options); + + if(video_codec_is_vulkan(settings->video_codec)) { + av_dict_set_int(&options, "async_depth", 3, 0); + av_dict_set(&options, "tune", "ll", 0); // Low latency + av_dict_set(&options, "usage", settings->is_livestream ? "stream" : "record", 0); + av_dict_set(&options, "content", "rendered", 0); // Game or 3D content + + if(codec_context->codec_id == AV_CODEC_ID_H264) { + // Removed because it causes stutter in games for some people + //av_dict_set_int(&options, "quality", 5, 0); // quality preset + } else if(codec_context->codec_id == AV_CODEC_ID_AV1) { + av_dict_set(&options, "tier", "main", 0); + } else if(codec_context->codec_id == AV_CODEC_ID_HEVC) { + if(hdr) + av_dict_set(&options, "sei", "hdr", 0); + } + } else if(egl->gpu_info.vendor == GSR_GPU_VENDOR_NVIDIA) { + // TODO: These dont seem to be necessary + // av_dict_set_int(&options, "zerolatency", 1, 0); + // if(codec_context->codec_id == AV_CODEC_ID_AV1) { + // av_dict_set(&options, "tune", "ll", 0); + // } else if(codec_context->codec_id == AV_CODEC_ID_H264 || codec_context->codec_id == AV_CODEC_ID_HEVC) { + // av_dict_set(&options, "preset", "llhq", 0); + // av_dict_set(&options, "tune", "ll", 0); + // } + av_dict_set(&options, "tune", "ll", 0); + av_dict_set_int(&options, "forced-idr", 1, 0); + + switch(settings->tune) { + case GSR_TUNE_PERFORMANCE: + //av_dict_set(&options, "multipass", "qres", 0); + break; + case GSR_TUNE_QUALITY: + av_dict_set(&options, "multipass", "fullres", 0); + av_dict_set(&options, "preset", "p6", 0); + av_dict_set_int(&options, "rc-lookahead", 0, 0); + break; + } + + if(codec_context->codec_id == AV_CODEC_ID_H264) { + // TODO: h264 10bit? + // TODO: + // switch(pixel_format) { + // case GSR_PIXEL_FORMAT_YUV420: + // av_dict_set_int(&options, "profile", AV_PROFILE_H264_HIGH, 0); + // break; + // case GSR_PIXEL_FORMAT_YUV444: + // av_dict_set_int(&options, "profile", AV_PROFILE_H264_HIGH_444, 0); + // break; + // } + } else if(codec_context->codec_id == AV_CODEC_ID_AV1) { + switch(settings->pixel_format) { + case GSR_PIXEL_FORMAT_YUV420: + av_dict_set(&options, "rgb_mode", "yuv420", 0); + break; + case GSR_PIXEL_FORMAT_YUV444: + av_dict_set(&options, "rgb_mode", "yuv444", 0); + break; + } + } else if(codec_context->codec_id == AV_CODEC_ID_HEVC) { + //av_dict_set(&options, "pix_fmt", "yuv420p16le", 0); + } + } else { + // TODO: More quality options + if(low_power) + av_dict_set_int(&options, "low_power", 1, 0); + // Improves performance but increases vram. + // TODO: Might need a different async_depth for optimal performance on different amd/intel gpus + av_dict_set_int(&options, "async_depth", 3, 0); + + if(codec_context->codec_id == AV_CODEC_ID_H264) { + // Removed because it causes stutter in games for some people + //av_dict_set_int(&options, "quality", 5, 0); // quality preset + } else if(codec_context->codec_id == AV_CODEC_ID_AV1) { + av_dict_set(&options, "tier", "main", 0); + } else if(codec_context->codec_id == AV_CODEC_ID_HEVC) { + if(hdr) + av_dict_set(&options, "sei", "hdr", 0); + } + + // TODO: vp8/vp9 10bit + } + + if(codec_context->codec_id == AV_CODEC_ID_H264) { + av_dict_set(&options, "coder", "cabac", 0); // TODO: cavlc is faster than cabac but worse compression. Which to use? + } + + av_dict_set(&options, "strict", "experimental", 0); + + if(settings->ffmpeg_video_opts) + av_dict_parse_string(&options, settings->ffmpeg_video_opts, "=", ";", 0); + + int ret = avcodec_open2(codec_context, codec_context->codec, &options); + if (ret < 0) { + gsr_log(GSR_LOG_LEVEL_ERROR, "Could not open video codec: %s", gsr_av_error_to_string(ret)); + return false; + } + + return true; +} + +enum AVPixelFormat get_pixel_format(gsr_video_codec video_codec, gsr_gpu_vendor vendor, bool use_software_video_encoder) { + if(use_software_video_encoder) { + return AV_PIX_FMT_NV12; + } else { + if(video_codec_is_vulkan(video_codec)) + return AV_PIX_FMT_VULKAN; + else + return vendor == GSR_GPU_VENDOR_NVIDIA ? AV_PIX_FMT_CUDA : AV_PIX_FMT_VAAPI; + } +} -- cgit v1.2.3 From d02955183135dbc72072492cdfcb35f2387e2c00 Mon Sep 17 00:00:00 2001 From: dec05eba Date: Sat, 1 Aug 2026 21:17:53 +0200 Subject: Move video/audio codec selection from main.cpp into codec_select module Codec selection returns gsr_error values instead of exiting, and takes gsr_recorder_settings instead of the argument parser. --- include/recorder/codec_select.h | 30 +++ include/recorder/error.h | 31 +++ meson.build | 1 + src/main.cpp | 516 +-------------------------------------- src/recorder/codec_select.c | 525 ++++++++++++++++++++++++++++++++++++++++ 5 files changed, 595 insertions(+), 508 deletions(-) create mode 100644 include/recorder/codec_select.h create mode 100644 include/recorder/error.h create mode 100644 src/recorder/codec_select.c diff --git a/include/recorder/codec_select.h b/include/recorder/codec_select.h new file mode 100644 index 0000000..ff4b064 --- /dev/null +++ b/include/recorder/codec_select.h @@ -0,0 +1,30 @@ +#ifndef GSR_RECORDER_CODEC_SELECT_H +#define GSR_RECORDER_CODEC_SELECT_H + +#include +#include "../defs.h" +#include "../egl.h" +#include "../vec2.h" +#include "../codec_query/codec_query.h" +#include "settings.h" + +#include + +typedef struct gsr_video_encoder gsr_video_encoder; + +gsr_video_encoder* create_video_encoder(gsr_egl *egl, const gsr_recorder_settings *settings); + +bool get_supported_video_codecs(gsr_egl *egl, gsr_video_codec video_codec, bool use_software_video_encoder, bool cleanup, gsr_supported_video_codecs *video_codecs); +void set_supported_video_codecs_ffmpeg(gsr_supported_video_codecs *supported_video_codecs, gsr_supported_video_codecs *supported_video_codecs_vulkan, gsr_gpu_vendor vendor); + +vec2i codec_get_max_resolution(gsr_video_codec video_codec, bool use_software_video_encoder, const gsr_supported_video_codecs *supported_video_codecs); +bool codec_supports_resolution(vec2i codec_max_resolution, vec2i capture_resolution); +/* Returns -1 if none is available */ +gsr_video_codec select_appropriate_video_codec_automatically(vec2i video_size, const gsr_supported_video_codecs *supported_video_codecs); +void force_cpu_encoding(gsr_recorder_settings *settings); + +gsr_audio_codec select_audio_codec_with_fallback(gsr_audio_codec audio_codec, const char *file_extension, bool uses_amix); +/* Returns a |gsr_error| value. |settings| video codec and encoder are updated to the codec that was selected */ +int select_video_codec_with_fallback(vec2i video_size, gsr_recorder_settings *settings, const char *file_extension, gsr_egl *egl, bool *low_power, const AVCodec **video_codec); + +#endif /* GSR_RECORDER_CODEC_SELECT_H */ diff --git a/include/recorder/error.h b/include/recorder/error.h new file mode 100644 index 0000000..75f1416 --- /dev/null +++ b/include/recorder/error.h @@ -0,0 +1,31 @@ +#ifndef GSR_RECORDER_ERROR_H +#define GSR_RECORDER_ERROR_H + +#ifdef __cplusplus +extern "C" { +#endif + +/* The negated value of each error is the exit code that gpu-screen-recorder exits with */ +typedef enum { + GSR_ERROR_OK = 0, + GSR_ERROR_GENERIC = -1, + GSR_ERROR_AUDIO_SETUP_FAILED = -2, + GSR_ERROR_CAPTURE_FAILED = -3, + GSR_ERROR_VIDEO_CODEC_QUERY_FAILED = -11, + GSR_ERROR_OPENGL_LOAD_FAILED = -22, + GSR_ERROR_AUDIO_DEVICE_NOT_FOUND = -50, + GSR_ERROR_MONITOR_NOT_FOUND = -51, + GSR_ERROR_NO_VIDEO_CODEC_AVAILABLE = -52, + GSR_ERROR_VIDEO_CODEC_RESOLUTION_UNSUPPORTED = -53, + GSR_ERROR_VIDEO_CODEC_UNSUPPORTED = -54 +} gsr_error; + +static inline int gsr_error_to_exit_code(int error) { + return error < 0 ? -error : 0; +} + +#ifdef __cplusplus +} +#endif + +#endif /* GSR_RECORDER_ERROR_H */ diff --git a/meson.build b/meson.build index 220ecbf..de7d2f2 100644 --- a/meson.build +++ b/meson.build @@ -34,6 +34,7 @@ src = [ 'src/ffmpeg_utils.c', 'src/recorder/audio_codec.c', 'src/recorder/video_codec.c', + 'src/recorder/codec_select.c', 'src/egl.c', 'src/cuda.c', 'src/window_texture.c', diff --git a/src/main.cpp b/src/main.cpp index b618a3b..70ef9e8 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -2,6 +2,8 @@ extern "C" { #include "../include/ffmpeg_utils.h" #include "../include/recorder/audio_codec.h" #include "../include/recorder/video_codec.h" +#include "../include/recorder/codec_select.h" +#include "../include/recorder/error.h" #include "../include/capture/nvfbc.h" #include "../include/capture/xcomposite.h" #include "../include/capture/ximage.h" @@ -811,72 +813,6 @@ fail: return err; } -static gsr_video_encoder* create_video_encoder(gsr_egl *egl, const args_parser &arg_parser) { - const gsr_color_depth color_depth = video_codec_to_bit_depth(arg_parser.settings.video_codec); - gsr_video_encoder *video_encoder = nullptr; - - if(arg_parser.settings.video_encoder == GSR_VIDEO_ENCODER_HW_CPU) { - gsr_video_encoder_software_params params; - params.egl = egl; - params.color_depth = color_depth; - video_encoder = gsr_video_encoder_software_create(¶ms); - return video_encoder; - } - - if(video_codec_is_vulkan(arg_parser.settings.video_codec)) { - gsr_video_encoder_vulkan_params params; - params.egl = egl; - params.color_depth = color_depth; - video_encoder = gsr_video_encoder_vulkan_create(¶ms); - return video_encoder; - } - - switch(egl->gpu_info.vendor) { - case GSR_GPU_VENDOR_AMD: - case GSR_GPU_VENDOR_INTEL: - case GSR_GPU_VENDOR_BROADCOM: { - gsr_video_encoder_vaapi_params params; - params.egl = egl; - params.color_depth = color_depth; - video_encoder = gsr_video_encoder_vaapi_create(¶ms); - break; - } - case GSR_GPU_VENDOR_NVIDIA: { - gsr_video_encoder_nvenc_params params; - params.egl = egl; - params.color_depth = color_depth; - video_encoder = gsr_video_encoder_nvenc_create(¶ms); - break; - } - } - - return video_encoder; -} - -static bool get_supported_video_codecs(gsr_egl *egl, gsr_video_codec video_codec, bool use_software_video_encoder, bool cleanup, gsr_supported_video_codecs *video_codecs) { - memset(video_codecs, 0, sizeof(*video_codecs)); - - if(use_software_video_encoder) { - video_codecs->h264.supported = avcodec_find_encoder_by_name("libx264"); - video_codecs->h264.max_resolution = {4096, 2304}; - return true; - } - - if(video_codec_is_vulkan(video_codec)) - return gsr_get_supported_video_codecs_vulkan(video_codecs, egl->card_path, &egl->vulkan_device_index, cleanup); - - switch(egl->gpu_info.vendor) { - case GSR_GPU_VENDOR_AMD: - case GSR_GPU_VENDOR_INTEL: - case GSR_GPU_VENDOR_BROADCOM: - return gsr_get_supported_video_codecs_vaapi(video_codecs, egl->card_path, cleanup); - case GSR_GPU_VENDOR_NVIDIA: - return gsr_get_supported_video_codecs_nvenc(video_codecs, cleanup); - } - - return false; -} - static void xwayland_check_callback(const gsr_monitor *monitor, void *userdata) { bool *xwayland_found = (bool*)userdata; if(monitor->name_len >= 8 && strncmp(monitor->name, "XWAYLAND", 8) == 0) @@ -949,80 +885,6 @@ static void list_gpu_info(gsr_egl *egl) { printf("card_path|%s\n", egl->card_path); } -static const AVCodec* get_ffmpeg_video_codec(gsr_video_codec video_codec, gsr_gpu_vendor vendor) { - switch(video_codec) { - case GSR_VIDEO_CODEC_H264: - return avcodec_find_encoder_by_name(vendor == GSR_GPU_VENDOR_NVIDIA ? "h264_nvenc" : "h264_vaapi"); - case GSR_VIDEO_CODEC_HEVC: - case GSR_VIDEO_CODEC_HEVC_HDR: - case GSR_VIDEO_CODEC_HEVC_10BIT: - return avcodec_find_encoder_by_name(vendor == GSR_GPU_VENDOR_NVIDIA ? "hevc_nvenc" : "hevc_vaapi"); - case GSR_VIDEO_CODEC_AV1: - case GSR_VIDEO_CODEC_AV1_HDR: - case GSR_VIDEO_CODEC_AV1_10BIT: - return avcodec_find_encoder_by_name(vendor == GSR_GPU_VENDOR_NVIDIA ? "av1_nvenc" : "av1_vaapi"); - case GSR_VIDEO_CODEC_VP8: - return avcodec_find_encoder_by_name(vendor == GSR_GPU_VENDOR_NVIDIA ? "vp8_nvenc" : "vp8_vaapi"); - case GSR_VIDEO_CODEC_VP9: - return avcodec_find_encoder_by_name(vendor == GSR_GPU_VENDOR_NVIDIA ? "vp9_nvenc" : "vp9_vaapi"); - case GSR_VIDEO_CODEC_H264_VULKAN: - return avcodec_find_encoder_by_name("h264_vulkan"); - case GSR_VIDEO_CODEC_HEVC_VULKAN: - case GSR_VIDEO_CODEC_HEVC_HDR_VULKAN: - case GSR_VIDEO_CODEC_HEVC_10BIT_VULKAN: - return avcodec_find_encoder_by_name("hevc_vulkan"); - case GSR_VIDEO_CODEC_AV1_VULKAN: - case GSR_VIDEO_CODEC_AV1_HDR_VULKAN: - case GSR_VIDEO_CODEC_AV1_10BIT_VULKAN: - return avcodec_find_encoder_by_name("av1_vulkan"); - } - return nullptr; -} - -static void set_supported_video_codecs_ffmpeg(gsr_supported_video_codecs *supported_video_codecs, gsr_supported_video_codecs *supported_video_codecs_vulkan, gsr_gpu_vendor vendor) { - if(!get_ffmpeg_video_codec(GSR_VIDEO_CODEC_H264, vendor)) { - supported_video_codecs->h264.supported = false; - } - - if(!get_ffmpeg_video_codec(GSR_VIDEO_CODEC_HEVC, vendor)) { - supported_video_codecs->hevc.supported = false; - supported_video_codecs->hevc_hdr.supported = false; - supported_video_codecs->hevc_10bit.supported = false; - } - - if(!get_ffmpeg_video_codec(GSR_VIDEO_CODEC_AV1, vendor)) { - supported_video_codecs->av1.supported = false; - supported_video_codecs->av1_hdr.supported = false; - supported_video_codecs->av1_10bit.supported = false; - } - - if(!get_ffmpeg_video_codec(GSR_VIDEO_CODEC_VP8, vendor)) { - supported_video_codecs->vp8.supported = false; - } - - if(!get_ffmpeg_video_codec(GSR_VIDEO_CODEC_VP9, vendor)) { - supported_video_codecs->vp9.supported = false; - } - - if(supported_video_codecs_vulkan) { - if(!get_ffmpeg_video_codec(GSR_VIDEO_CODEC_H264_VULKAN, vendor)) { - supported_video_codecs_vulkan->h264.supported = false; - } - - if(!get_ffmpeg_video_codec(GSR_VIDEO_CODEC_HEVC_VULKAN, vendor)) { - supported_video_codecs_vulkan->hevc.supported = false; - supported_video_codecs_vulkan->hevc_hdr.supported = false; - supported_video_codecs_vulkan->hevc_10bit.supported = false; - } - - if(!get_ffmpeg_video_codec(GSR_VIDEO_CODEC_AV1_VULKAN, vendor)) { - supported_video_codecs_vulkan->av1.supported = false; - supported_video_codecs_vulkan->av1_hdr.supported = false; - supported_video_codecs_vulkan->av1_10bit.supported = false; - } - } -} - 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; @@ -2364,372 +2226,7 @@ static void validate_merged_audio_inputs_app_audio(const std::vectorh264.supported) - return get_ffmpeg_video_codec(video_codec, egl->gpu_info.vendor); - break; - } - case GSR_VIDEO_CODEC_HEVC: - case GSR_VIDEO_CODEC_HEVC_VULKAN: { - if(supported_video_codecs->hevc.supported) - return get_ffmpeg_video_codec(video_codec, egl->gpu_info.vendor); - break; - } - case GSR_VIDEO_CODEC_HEVC_HDR: - case GSR_VIDEO_CODEC_HEVC_HDR_VULKAN: { - if(supported_video_codecs->hevc_hdr.supported) - return get_ffmpeg_video_codec(video_codec, egl->gpu_info.vendor); - break; - } - case GSR_VIDEO_CODEC_HEVC_10BIT: - case GSR_VIDEO_CODEC_HEVC_10BIT_VULKAN: { - if(supported_video_codecs->hevc_10bit.supported) - return get_ffmpeg_video_codec(video_codec, egl->gpu_info.vendor); - break; - } - case GSR_VIDEO_CODEC_AV1: - case GSR_VIDEO_CODEC_AV1_VULKAN: { - if(supported_video_codecs->av1.supported) - return get_ffmpeg_video_codec(video_codec, egl->gpu_info.vendor); - break; - } - case GSR_VIDEO_CODEC_AV1_HDR: - case GSR_VIDEO_CODEC_AV1_HDR_VULKAN: { - if(supported_video_codecs->av1_hdr.supported) - return get_ffmpeg_video_codec(video_codec, egl->gpu_info.vendor); - break; - } - case GSR_VIDEO_CODEC_AV1_10BIT: - case GSR_VIDEO_CODEC_AV1_10BIT_VULKAN: { - if(supported_video_codecs->av1_10bit.supported) - return get_ffmpeg_video_codec(video_codec, egl->gpu_info.vendor); - break; - } - case GSR_VIDEO_CODEC_VP8: { - if(supported_video_codecs->vp8.supported) - return get_ffmpeg_video_codec(video_codec, egl->gpu_info.vendor); - break; - } - case GSR_VIDEO_CODEC_VP9: { - if(supported_video_codecs->vp9.supported) - return get_ffmpeg_video_codec(video_codec, egl->gpu_info.vendor); - break; - } - } - return nullptr; -} - -static vec2i codec_get_max_resolution(gsr_video_codec video_codec, bool use_software_video_encoder, const gsr_supported_video_codecs *supported_video_codecs) { - switch(video_codec) { - case GSR_VIDEO_CODEC_H264: - case GSR_VIDEO_CODEC_H264_VULKAN: { - if(use_software_video_encoder) - return {4096, 2304}; - else if(supported_video_codecs->h264.supported) - return supported_video_codecs->h264.max_resolution; - break; - } - case GSR_VIDEO_CODEC_HEVC: - case GSR_VIDEO_CODEC_HEVC_VULKAN: { - if(supported_video_codecs->hevc.supported) - return supported_video_codecs->hevc.max_resolution; - break; - } - case GSR_VIDEO_CODEC_HEVC_HDR: - case GSR_VIDEO_CODEC_HEVC_HDR_VULKAN: { - if(supported_video_codecs->hevc_hdr.supported) - return supported_video_codecs->hevc_hdr.max_resolution; - break; - } - case GSR_VIDEO_CODEC_HEVC_10BIT: - case GSR_VIDEO_CODEC_HEVC_10BIT_VULKAN: { - if(supported_video_codecs->hevc_10bit.supported) - return supported_video_codecs->hevc_10bit.max_resolution; - break; - } - case GSR_VIDEO_CODEC_AV1: - case GSR_VIDEO_CODEC_AV1_VULKAN: { - if(supported_video_codecs->av1.supported) - return supported_video_codecs->av1.max_resolution; - break; - } - case GSR_VIDEO_CODEC_AV1_HDR: - case GSR_VIDEO_CODEC_AV1_HDR_VULKAN: { - if(supported_video_codecs->av1_hdr.supported) - return supported_video_codecs->av1_hdr.max_resolution; - break; - } - case GSR_VIDEO_CODEC_AV1_10BIT: - case GSR_VIDEO_CODEC_AV1_10BIT_VULKAN: { - if(supported_video_codecs->av1_10bit.supported) - return supported_video_codecs->av1_10bit.max_resolution; - break; - } - case GSR_VIDEO_CODEC_VP8: { - if(supported_video_codecs->vp8.supported) - return supported_video_codecs->vp8.max_resolution; - break; - } - case GSR_VIDEO_CODEC_VP9: { - if(supported_video_codecs->vp9.supported) - return supported_video_codecs->vp9.max_resolution; - break; - } - } - return {0, 0}; -} - -static bool codec_supports_resolution(vec2i codec_max_resolution, vec2i capture_resolution) { - if(codec_max_resolution.x == 0 || codec_max_resolution.y == 0) - return true; - return codec_max_resolution.x >= capture_resolution.x && codec_max_resolution.y >= capture_resolution.y; -} - -static void print_codec_error(gsr_video_codec video_codec) { - if(video_codec == (gsr_video_codec)GSR_VIDEO_CODEC_AUTO) - video_codec = GSR_VIDEO_CODEC_H264; - - const char *video_codec_name = video_codec_to_string(video_codec); - gsr_log(GSR_LOG_LEVEL_ERROR, "your gpu does not support '%s' video codec. If you are sure that your gpu does support '%s' video encoding and you are using an AMD/Intel GPU,\n then make sure you have installed the GPU specific vaapi packages (intel-media-driver, libva-intel-driver, libva-mesa-driver and linux-firmware).\n It's also possible that your distro has disabled hardware accelerated video encoding for '%s' video codec.\n This may be the case on corporate distros such as Manjaro, Fedora or OpenSUSE.\n You can test this by running 'vainfo | grep VAEntrypointEncSlice' to see if it matches any H264/HEVC/AV1/VP8/VP9 profile.\n On such distros, you need to manually install mesa from source to enable H264/HEVC hardware acceleration, or use a more user friendly distro. Alternatively record with AV1 if supported by your GPU.\n You can alternatively use the flatpak version of GPU Screen Recorder (https://flathub.org/apps/com.dec05eba.gpu_screen_recorder) which bypasses system issues with patented H264/HEVC codecs.\n If your GPU doesn't support hardware accelerated video encoding then you can use '-fallback-cpu-encoding yes' option to encode with your cpu instead.", video_codec_name, video_codec_name, video_codec_name); -} - -static void force_cpu_encoding(args_parser *args_parser) { - args_parser->settings.video_codec = GSR_VIDEO_CODEC_H264; - args_parser->settings.video_encoder = GSR_VIDEO_ENCODER_HW_CPU; - if(args_parser->settings.bitrate_mode == GSR_BITRATE_MODE_VBR) { - gsr_log(GSR_LOG_LEVEL_WARNING, "bitrate mode has been forcefully set to qp because software encoding option doesn't support vbr option"); - args_parser->settings.bitrate_mode = GSR_BITRATE_MODE_QP; - } -} - -static const AVCodec* pick_video_codec(gsr_egl *egl, args_parser *args_parser, bool use_fallback_codec, bool *low_power, gsr_supported_video_codecs *supported_video_codecs) { - // TODO: software encoder for hevc, av1, vp8 and vp9 - *low_power = false; - const AVCodec *video_codec_f = get_av_codec_if_supported(args_parser->settings.video_codec, egl, args_parser->settings.video_encoder == GSR_VIDEO_ENCODER_HW_CPU, supported_video_codecs); - - if(!video_codec_f && use_fallback_codec && args_parser->settings.video_encoder != GSR_VIDEO_ENCODER_HW_CPU) { - switch(args_parser->settings.video_codec) { - case GSR_VIDEO_CODEC_H264: { - gsr_log(GSR_LOG_LEVEL_ERROR, "selected video codec h264 is not supported by your hardware"); - if(args_parser->settings.fallback_cpu_encoding) { - gsr_log(GSR_LOG_LEVEL_WARNING, "gpu encoding is not available on your system, trying cpu encoding instead because -fallback-cpu-encoding is enabled. Install the proper vaapi drivers on your system (if supported) if you experience performance issues"); - force_cpu_encoding(args_parser); - } - break; - } - case GSR_VIDEO_CODEC_HEVC: - case GSR_VIDEO_CODEC_HEVC_HDR: - case GSR_VIDEO_CODEC_HEVC_10BIT: { - gsr_log(GSR_LOG_LEVEL_WARNING, "selected video codec hevc is not supported by your hardware, trying h264 instead"); - args_parser->settings.video_codec = GSR_VIDEO_CODEC_H264; - return pick_video_codec(egl, args_parser, true, low_power, supported_video_codecs); - } - case GSR_VIDEO_CODEC_AV1: - case GSR_VIDEO_CODEC_AV1_HDR: - case GSR_VIDEO_CODEC_AV1_10BIT: { - gsr_log(GSR_LOG_LEVEL_WARNING, "selected video codec av1 is not supported by your hardware, trying h264 instead"); - args_parser->settings.video_codec = GSR_VIDEO_CODEC_H264; - return pick_video_codec(egl, args_parser, true, low_power, supported_video_codecs); - } - case GSR_VIDEO_CODEC_VP8: - case GSR_VIDEO_CODEC_VP9: - // TODO: Cant fallback to other codec because webm only supports vp8/vp9 - break; - case GSR_VIDEO_CODEC_H264_VULKAN: { - gsr_log(GSR_LOG_LEVEL_WARNING, "selected video codec h264_vulkan is not supported by your hardware, trying h264 instead"); - args_parser->settings.video_codec = GSR_VIDEO_CODEC_H264; - // Need to do a query again because this time it's without vulkan - if(!get_supported_video_codecs(egl, args_parser->settings.video_codec, false, true, supported_video_codecs)) { - gsr_log(GSR_LOG_LEVEL_ERROR, "failed to query for supported video codecs"); - print_codec_error(args_parser->settings.video_codec); - _exit(11); - } - return pick_video_codec(egl, args_parser, true, low_power, supported_video_codecs); - } - case GSR_VIDEO_CODEC_HEVC_VULKAN: - case GSR_VIDEO_CODEC_HEVC_HDR_VULKAN: - case GSR_VIDEO_CODEC_HEVC_10BIT_VULKAN: { - gsr_log(GSR_LOG_LEVEL_WARNING, "selected video codec hevc_vulkan is not supported by your hardware, trying hevc instead"); - args_parser->settings.video_codec = GSR_VIDEO_CODEC_HEVC; - // Need to do a query again because this time it's without vulkan - if(!get_supported_video_codecs(egl, args_parser->settings.video_codec, false, true, supported_video_codecs)) { - gsr_log(GSR_LOG_LEVEL_ERROR, "failed to query for supported video codecs"); - print_codec_error(args_parser->settings.video_codec); - _exit(11); - } - return pick_video_codec(egl, args_parser, true, low_power, supported_video_codecs); - } - case GSR_VIDEO_CODEC_AV1_VULKAN: - case GSR_VIDEO_CODEC_AV1_HDR_VULKAN: - case GSR_VIDEO_CODEC_AV1_10BIT_VULKAN: { - gsr_log(GSR_LOG_LEVEL_WARNING, "selected video codec av1_vulkan is not supported by your hardware, trying av1 instead"); - args_parser->settings.video_codec = GSR_VIDEO_CODEC_AV1; - // Need to do a query again because this time it's without vulkan - if(!get_supported_video_codecs(egl, args_parser->settings.video_codec, false, true, supported_video_codecs)) { - gsr_log(GSR_LOG_LEVEL_ERROR, "failed to query for supported video codecs"); - print_codec_error(args_parser->settings.video_codec); - _exit(11); - } - return pick_video_codec(egl, args_parser, true, low_power, supported_video_codecs); - } - } - - video_codec_f = get_av_codec_if_supported(args_parser->settings.video_codec, egl, args_parser->settings.video_encoder == GSR_VIDEO_ENCODER_HW_CPU, supported_video_codecs); - } - - if(!video_codec_f) { - print_codec_error(args_parser->settings.video_codec); - _exit(54); - } - - *low_power = video_codec_only_supports_low_power_mode(*supported_video_codecs, args_parser->settings.video_codec); - - return video_codec_f; -} - /* Returns -1 if none is available */ -static gsr_video_codec select_appropriate_video_codec_automatically(vec2i video_size, const gsr_supported_video_codecs *supported_video_codecs) { - if(supported_video_codecs->h264.supported && codec_supports_resolution(supported_video_codecs->h264.max_resolution, video_size)) { - gsr_log(GSR_LOG_LEVEL_INFO, "using h264 encoder because a codec was not specified"); - return GSR_VIDEO_CODEC_H264; - } else if(supported_video_codecs->hevc.supported && codec_supports_resolution(supported_video_codecs->hevc.max_resolution, video_size)) { - gsr_log(GSR_LOG_LEVEL_INFO, "using hevc encoder because a codec was not specified and h264 supported max resolution (%dx%d) is less than the capture resolution (%dx%d)", supported_video_codecs->h264.max_resolution.x, supported_video_codecs->h264.max_resolution.y, - video_size.x, video_size.y); - return GSR_VIDEO_CODEC_HEVC; - } else if(supported_video_codecs->av1.supported && codec_supports_resolution(supported_video_codecs->av1.max_resolution, video_size)) { - gsr_log(GSR_LOG_LEVEL_INFO, "using av1 encoder because a codec was not specified and hevc supported max resolution (%dx%d) is less than the capture resolution (%dx%d)", supported_video_codecs->hevc.max_resolution.x, supported_video_codecs->hevc.max_resolution.y, - video_size.x, video_size.y); - return GSR_VIDEO_CODEC_AV1; - } else { - return (gsr_video_codec)-1; - } -} - -static const AVCodec* select_video_codec_with_fallback(vec2i video_size, args_parser *args_parser, const char *file_extension, gsr_egl *egl, bool *low_power) { - gsr_supported_video_codecs supported_video_codecs_non_vulkan; - get_supported_video_codecs(egl, args_parser->settings.video_codec, args_parser->settings.video_encoder == GSR_VIDEO_ENCODER_HW_CPU, true, &supported_video_codecs_non_vulkan); - - gsr_supported_video_codecs supported_video_codecs_vulkan = supported_video_codecs_non_vulkan; - set_supported_video_codecs_ffmpeg(&supported_video_codecs_non_vulkan, &supported_video_codecs_vulkan, egl->gpu_info.vendor); - - gsr_supported_video_codecs *supported_video_codecs = video_codec_is_vulkan(args_parser->settings.video_codec) - ? &supported_video_codecs_vulkan - : &supported_video_codecs_non_vulkan; - - const bool video_codec_auto = args_parser->settings.video_codec == (gsr_video_codec)GSR_VIDEO_CODEC_AUTO; - if(video_codec_auto) { - if(strcmp(file_extension, "webm") == 0) { - gsr_log(GSR_LOG_LEVEL_INFO, "using vp8 encoder because a codec was not specified and the file extension is .webm"); - args_parser->settings.video_codec = GSR_VIDEO_CODEC_VP8; - } else if(args_parser->settings.video_encoder == GSR_VIDEO_ENCODER_HW_CPU) { - gsr_log(GSR_LOG_LEVEL_INFO, "using h264 encoder because a codec was not specified"); - args_parser->settings.video_codec = GSR_VIDEO_CODEC_H264; - } else if(args_parser->settings.video_encoder != GSR_VIDEO_ENCODER_HW_CPU) { - args_parser->settings.video_codec = select_appropriate_video_codec_automatically(video_size, &supported_video_codecs_non_vulkan); - if(args_parser->settings.video_codec == (gsr_video_codec)-1) { - if(args_parser->settings.fallback_cpu_encoding) { - gsr_log(GSR_LOG_LEVEL_WARNING, "gpu encoding is not available on your system or your gpu doesn't support recording at the resolution you are trying to record, trying cpu encoding instead because -fallback-cpu-encoding is enabled. Install the proper vaapi drivers on your system (if supported) if you experience performance issues"); - force_cpu_encoding(args_parser); - } else { - gsr_log(GSR_LOG_LEVEL_ERROR, "no video encoder was specified and neither h264, hevc nor av1 are supported on your system or you are trying to capture at a resolution higher than your system supports for each codec."); - fprintf(stderr, " Ensure that you have installed the proper vaapi driver. If your gpu doesn't support video encoding then you can run gpu-screen-recorder with \"-fallback-cpu-encoding yes\" option to use cpu encoding.\n"); - _exit(52); - } - } - } - } - - if(LIBAVFORMAT_VERSION_INT < AV_VERSION_INT(60, 10, 100) && strcmp(file_extension, "flv") == 0) { - if(args_parser->settings.video_codec != GSR_VIDEO_CODEC_H264) { - args_parser->settings.video_codec = GSR_VIDEO_CODEC_H264; - gsr_log(GSR_LOG_LEVEL_WARNING, "hevc/av1 is not compatible with flv in your outdated version of ffmpeg, falling back to h264 instead."); - } - } else if(strcmp(file_extension, "m3u8") == 0) { - if(video_codec_is_av1(args_parser->settings.video_codec)) { - args_parser->settings.video_codec = GSR_VIDEO_CODEC_HEVC; - gsr_log(GSR_LOG_LEVEL_WARNING, "av1 is not compatible with hls (m3u8), falling back to hevc instead."); - } - } - - const AVCodec *codec = pick_video_codec(egl, args_parser, true, low_power, supported_video_codecs); - - const vec2i codec_max_resolution = codec_get_max_resolution(args_parser->settings.video_codec, args_parser->settings.video_encoder == GSR_VIDEO_ENCODER_HW_CPU, supported_video_codecs); - if(!codec_supports_resolution(codec_max_resolution, video_size)) { - const char *video_codec_name = video_codec_to_string(args_parser->settings.video_codec); - gsr_log(GSR_LOG_LEVEL_ERROR, "The max resolution for video codec %s is %dx%d while you are trying to capture at resolution %dx%d. Change capture resolution or video codec and try again", video_codec_name, codec_max_resolution.x, codec_max_resolution.y, video_size.x, video_size.y); - _exit(53); - } - - return codec; -} - static std::vector create_device_audio_inputs(const std::vector &audio_inputs, AVCodecContext *audio_codec_context, int num_channels, double num_audio_frames_shift, std::vector &src_filter_ctx, bool use_amix) { std::vector audio_track_audio_devices; for(size_t i = 0; i < audio_inputs.size(); ++i) { @@ -3224,7 +2721,7 @@ int main(int argc, char **argv) { const double target_fps = 1.0 / (double)arg_parser.settings.fps; const bool uses_amix = merged_audio_inputs_should_use_amix(requested_audio_inputs); - arg_parser.settings.audio_codec = select_audio_codec_with_fallback(arg_parser.settings.audio_codec, file_extension, uses_amix); + arg_parser.settings.audio_codec = select_audio_codec_with_fallback(arg_parser.settings.audio_codec, file_extension.c_str(), uses_amix); vec2i video_size = {0, 0}; std::vector video_sources = create_video_sources(arg_parser, &egl, false, capture_sources, video_size); @@ -3247,7 +2744,10 @@ int main(int argc, char **argv) { } bool low_power = false; - const AVCodec *video_codec_f = select_video_codec_with_fallback(video_size, &arg_parser, file_extension.c_str(), &egl, &low_power); + const AVCodec *video_codec_f = nullptr; + const int select_video_codec_result = select_video_codec_with_fallback(video_size, &arg_parser.settings, file_extension.c_str(), &egl, &low_power, &video_codec_f); + if(select_video_codec_result != GSR_ERROR_OK) + _exit(gsr_error_to_exit_code(select_video_codec_result)); const enum AVPixelFormat video_pix_fmt = get_pixel_format(arg_parser.settings.video_codec, egl.gpu_info.vendor, arg_parser.settings.video_encoder == GSR_VIDEO_ENCODER_HW_CPU); AVCodecContext *video_codec_context = create_video_codec_context(video_pix_fmt, video_codec_f, &egl, &arg_parser.settings, video_size.x, video_size.y); @@ -3275,7 +2775,7 @@ int main(int argc, char **argv) { _exit(1); } - gsr_video_encoder *video_encoder = create_video_encoder(&egl, arg_parser); + gsr_video_encoder *video_encoder = create_video_encoder(&egl, &arg_parser.settings); if(!video_encoder) { gsr_log(GSR_LOG_LEVEL_ERROR, "failed to create video encoder"); _exit(1); diff --git a/src/recorder/codec_select.c b/src/recorder/codec_select.c new file mode 100644 index 0000000..d3286ec --- /dev/null +++ b/src/recorder/codec_select.c @@ -0,0 +1,525 @@ +#include "../../include/recorder/codec_select.h" +#include "../../include/recorder/error.h" +#include "../../include/encoder/video/nvenc.h" +#include "../../include/encoder/video/vaapi.h" +#include "../../include/encoder/video/vulkan.h" +#include "../../include/encoder/video/software.h" +#include "../../include/codec_query/nvenc.h" +#include "../../include/codec_query/vaapi.h" +#include "../../include/codec_query/vulkan.h" +#include "../../include/log.h" + +#include + +#include +#include + +gsr_video_encoder* create_video_encoder(gsr_egl *egl, const gsr_recorder_settings *settings) { + const gsr_color_depth color_depth = video_codec_to_bit_depth(settings->video_codec); + gsr_video_encoder *video_encoder = NULL; + + if(settings->video_encoder == GSR_VIDEO_ENCODER_HW_CPU) { + gsr_video_encoder_software_params params; + params.egl = egl; + params.color_depth = color_depth; + video_encoder = gsr_video_encoder_software_create(¶ms); + return video_encoder; + } + + if(video_codec_is_vulkan(settings->video_codec)) { + gsr_video_encoder_vulkan_params params; + params.egl = egl; + params.color_depth = color_depth; + video_encoder = gsr_video_encoder_vulkan_create(¶ms); + return video_encoder; + } + + switch(egl->gpu_info.vendor) { + case GSR_GPU_VENDOR_AMD: + case GSR_GPU_VENDOR_INTEL: + case GSR_GPU_VENDOR_BROADCOM: { + gsr_video_encoder_vaapi_params params; + params.egl = egl; + params.color_depth = color_depth; + video_encoder = gsr_video_encoder_vaapi_create(¶ms); + break; + } + case GSR_GPU_VENDOR_NVIDIA: { + gsr_video_encoder_nvenc_params params; + params.egl = egl; + params.color_depth = color_depth; + video_encoder = gsr_video_encoder_nvenc_create(¶ms); + break; + } + } + + return video_encoder; +} + +bool get_supported_video_codecs(gsr_egl *egl, gsr_video_codec video_codec, bool use_software_video_encoder, bool cleanup, gsr_supported_video_codecs *video_codecs) { + memset(video_codecs, 0, sizeof(*video_codecs)); + + if(use_software_video_encoder) { + video_codecs->h264.supported = avcodec_find_encoder_by_name("libx264"); + video_codecs->h264.max_resolution = (vec2i){4096, 2304}; + return true; + } + + if(video_codec_is_vulkan(video_codec)) + return gsr_get_supported_video_codecs_vulkan(video_codecs, egl->card_path, &egl->vulkan_device_index, cleanup); + + switch(egl->gpu_info.vendor) { + case GSR_GPU_VENDOR_AMD: + case GSR_GPU_VENDOR_INTEL: + case GSR_GPU_VENDOR_BROADCOM: + return gsr_get_supported_video_codecs_vaapi(video_codecs, egl->card_path, cleanup); + case GSR_GPU_VENDOR_NVIDIA: + return gsr_get_supported_video_codecs_nvenc(video_codecs, cleanup); + } + + return false; +} + +static const AVCodec* get_ffmpeg_video_codec(gsr_video_codec video_codec, gsr_gpu_vendor vendor) { + switch(video_codec) { + case GSR_VIDEO_CODEC_H264: + return avcodec_find_encoder_by_name(vendor == GSR_GPU_VENDOR_NVIDIA ? "h264_nvenc" : "h264_vaapi"); + case GSR_VIDEO_CODEC_HEVC: + case GSR_VIDEO_CODEC_HEVC_HDR: + case GSR_VIDEO_CODEC_HEVC_10BIT: + return avcodec_find_encoder_by_name(vendor == GSR_GPU_VENDOR_NVIDIA ? "hevc_nvenc" : "hevc_vaapi"); + case GSR_VIDEO_CODEC_AV1: + case GSR_VIDEO_CODEC_AV1_HDR: + case GSR_VIDEO_CODEC_AV1_10BIT: + return avcodec_find_encoder_by_name(vendor == GSR_GPU_VENDOR_NVIDIA ? "av1_nvenc" : "av1_vaapi"); + case GSR_VIDEO_CODEC_VP8: + return avcodec_find_encoder_by_name(vendor == GSR_GPU_VENDOR_NVIDIA ? "vp8_nvenc" : "vp8_vaapi"); + case GSR_VIDEO_CODEC_VP9: + return avcodec_find_encoder_by_name(vendor == GSR_GPU_VENDOR_NVIDIA ? "vp9_nvenc" : "vp9_vaapi"); + case GSR_VIDEO_CODEC_H264_VULKAN: + return avcodec_find_encoder_by_name("h264_vulkan"); + case GSR_VIDEO_CODEC_HEVC_VULKAN: + case GSR_VIDEO_CODEC_HEVC_HDR_VULKAN: + case GSR_VIDEO_CODEC_HEVC_10BIT_VULKAN: + return avcodec_find_encoder_by_name("hevc_vulkan"); + case GSR_VIDEO_CODEC_AV1_VULKAN: + case GSR_VIDEO_CODEC_AV1_HDR_VULKAN: + case GSR_VIDEO_CODEC_AV1_10BIT_VULKAN: + return avcodec_find_encoder_by_name("av1_vulkan"); + } + return NULL; +} + +void set_supported_video_codecs_ffmpeg(gsr_supported_video_codecs *supported_video_codecs, gsr_supported_video_codecs *supported_video_codecs_vulkan, gsr_gpu_vendor vendor) { + if(!get_ffmpeg_video_codec(GSR_VIDEO_CODEC_H264, vendor)) { + supported_video_codecs->h264.supported = false; + } + + if(!get_ffmpeg_video_codec(GSR_VIDEO_CODEC_HEVC, vendor)) { + supported_video_codecs->hevc.supported = false; + supported_video_codecs->hevc_hdr.supported = false; + supported_video_codecs->hevc_10bit.supported = false; + } + + if(!get_ffmpeg_video_codec(GSR_VIDEO_CODEC_AV1, vendor)) { + supported_video_codecs->av1.supported = false; + supported_video_codecs->av1_hdr.supported = false; + supported_video_codecs->av1_10bit.supported = false; + } + + if(!get_ffmpeg_video_codec(GSR_VIDEO_CODEC_VP8, vendor)) { + supported_video_codecs->vp8.supported = false; + } + + if(!get_ffmpeg_video_codec(GSR_VIDEO_CODEC_VP9, vendor)) { + supported_video_codecs->vp9.supported = false; + } + + if(supported_video_codecs_vulkan) { + if(!get_ffmpeg_video_codec(GSR_VIDEO_CODEC_H264_VULKAN, vendor)) { + supported_video_codecs_vulkan->h264.supported = false; + } + + if(!get_ffmpeg_video_codec(GSR_VIDEO_CODEC_HEVC_VULKAN, vendor)) { + supported_video_codecs_vulkan->hevc.supported = false; + supported_video_codecs_vulkan->hevc_hdr.supported = false; + supported_video_codecs_vulkan->hevc_10bit.supported = false; + } + + if(!get_ffmpeg_video_codec(GSR_VIDEO_CODEC_AV1_VULKAN, vendor)) { + supported_video_codecs_vulkan->av1.supported = false; + supported_video_codecs_vulkan->av1_hdr.supported = false; + supported_video_codecs_vulkan->av1_10bit.supported = false; + } + } +} + +gsr_audio_codec select_audio_codec_with_fallback(gsr_audio_codec audio_codec, const char *file_extension, bool uses_amix) { + switch(audio_codec) { + case GSR_AUDIO_CODEC_AAC: { + if(strcmp(file_extension, "webm") == 0) { + //audio_codec_to_use = "opus"; + audio_codec = GSR_AUDIO_CODEC_OPUS; + gsr_log(GSR_LOG_LEVEL_WARNING, ".webm files only support opus audio codec, changing audio codec from aac to opus"); + } + break; + } + case GSR_AUDIO_CODEC_OPUS: { + if(strcmp(file_extension, "mp4") != 0 && strcmp(file_extension, "mkv") != 0 && strcmp(file_extension, "webm") != 0 && strcmp(file_extension, "ts") != 0 && strcmp(file_extension, "whip") != 0) { + //audio_codec_to_use = "aac"; + audio_codec = GSR_AUDIO_CODEC_AAC; + gsr_log(GSR_LOG_LEVEL_WARNING, "opus audio codec is only supported by .mp4, .mkv, .webm and .ts files, falling back to aac instead"); + } + break; + } + case GSR_AUDIO_CODEC_FLAC: { + // TODO: Also check mpegts? + if(strcmp(file_extension, "webm") == 0) { + //audio_codec_to_use = "opus"; + audio_codec = GSR_AUDIO_CODEC_OPUS; + gsr_log(GSR_LOG_LEVEL_WARNING, ".webm files only support opus audio codec, changing audio codec from flac to opus"); + } else if(strcmp(file_extension, "mp4") != 0 && strcmp(file_extension, "mkv") != 0) { + //audio_codec_to_use = "aac"; + audio_codec = GSR_AUDIO_CODEC_AAC; + gsr_log(GSR_LOG_LEVEL_WARNING, "flac audio codec is only supported by .mp4 and .mkv files, falling back to aac instead"); + } else if(uses_amix) { + // TODO: remove this? is it true anymore? + //audio_codec_to_use = "opus"; + audio_codec = GSR_AUDIO_CODEC_OPUS; + gsr_log(GSR_LOG_LEVEL_WARNING, "flac audio codec is not supported when mixing audio sources, falling back to opus instead"); + } + break; + } + } + return audio_codec; +} + +static bool video_codec_only_supports_low_power_mode(const gsr_supported_video_codecs *supported_video_codecs, gsr_video_codec video_codec) { + switch(video_codec) { + case GSR_VIDEO_CODEC_H264: return supported_video_codecs->h264.low_power; + case GSR_VIDEO_CODEC_HEVC: return supported_video_codecs->hevc.low_power; + case GSR_VIDEO_CODEC_HEVC_HDR: return supported_video_codecs->hevc_hdr.low_power; + case GSR_VIDEO_CODEC_HEVC_10BIT: return supported_video_codecs->hevc_10bit.low_power; + case GSR_VIDEO_CODEC_AV1: return supported_video_codecs->av1.low_power; + case GSR_VIDEO_CODEC_AV1_HDR: return supported_video_codecs->av1_hdr.low_power; + case GSR_VIDEO_CODEC_AV1_10BIT: return supported_video_codecs->av1_10bit.low_power; + case GSR_VIDEO_CODEC_VP8: return supported_video_codecs->vp8.low_power; + case GSR_VIDEO_CODEC_VP9: return supported_video_codecs->vp9.low_power; + case GSR_VIDEO_CODEC_H264_VULKAN: return supported_video_codecs->h264.low_power; + case GSR_VIDEO_CODEC_HEVC_VULKAN: return supported_video_codecs->hevc.low_power; + case GSR_VIDEO_CODEC_HEVC_HDR_VULKAN: return supported_video_codecs->hevc_hdr.low_power; + case GSR_VIDEO_CODEC_HEVC_10BIT_VULKAN: return supported_video_codecs->hevc_10bit.low_power; + case GSR_VIDEO_CODEC_AV1_VULKAN: return supported_video_codecs->av1.low_power; + case GSR_VIDEO_CODEC_AV1_HDR_VULKAN: return supported_video_codecs->av1_hdr.low_power; + case GSR_VIDEO_CODEC_AV1_10BIT_VULKAN: return supported_video_codecs->av1_10bit.low_power; + } + return false; +} + +static const AVCodec* get_av_codec_if_supported(gsr_video_codec video_codec, gsr_egl *egl, bool use_software_video_encoder, const gsr_supported_video_codecs *supported_video_codecs) { + switch(video_codec) { + case GSR_VIDEO_CODEC_H264: + case GSR_VIDEO_CODEC_H264_VULKAN: { + if(use_software_video_encoder) + return avcodec_find_encoder_by_name("libx264"); + else if(supported_video_codecs->h264.supported) + return get_ffmpeg_video_codec(video_codec, egl->gpu_info.vendor); + break; + } + case GSR_VIDEO_CODEC_HEVC: + case GSR_VIDEO_CODEC_HEVC_VULKAN: { + if(supported_video_codecs->hevc.supported) + return get_ffmpeg_video_codec(video_codec, egl->gpu_info.vendor); + break; + } + case GSR_VIDEO_CODEC_HEVC_HDR: + case GSR_VIDEO_CODEC_HEVC_HDR_VULKAN: { + if(supported_video_codecs->hevc_hdr.supported) + return get_ffmpeg_video_codec(video_codec, egl->gpu_info.vendor); + break; + } + case GSR_VIDEO_CODEC_HEVC_10BIT: + case GSR_VIDEO_CODEC_HEVC_10BIT_VULKAN: { + if(supported_video_codecs->hevc_10bit.supported) + return get_ffmpeg_video_codec(video_codec, egl->gpu_info.vendor); + break; + } + case GSR_VIDEO_CODEC_AV1: + case GSR_VIDEO_CODEC_AV1_VULKAN: { + if(supported_video_codecs->av1.supported) + return get_ffmpeg_video_codec(video_codec, egl->gpu_info.vendor); + break; + } + case GSR_VIDEO_CODEC_AV1_HDR: + case GSR_VIDEO_CODEC_AV1_HDR_VULKAN: { + if(supported_video_codecs->av1_hdr.supported) + return get_ffmpeg_video_codec(video_codec, egl->gpu_info.vendor); + break; + } + case GSR_VIDEO_CODEC_AV1_10BIT: + case GSR_VIDEO_CODEC_AV1_10BIT_VULKAN: { + if(supported_video_codecs->av1_10bit.supported) + return get_ffmpeg_video_codec(video_codec, egl->gpu_info.vendor); + break; + } + case GSR_VIDEO_CODEC_VP8: { + if(supported_video_codecs->vp8.supported) + return get_ffmpeg_video_codec(video_codec, egl->gpu_info.vendor); + break; + } + case GSR_VIDEO_CODEC_VP9: { + if(supported_video_codecs->vp9.supported) + return get_ffmpeg_video_codec(video_codec, egl->gpu_info.vendor); + break; + } + } + return NULL; +} + +vec2i codec_get_max_resolution(gsr_video_codec video_codec, bool use_software_video_encoder, const gsr_supported_video_codecs *supported_video_codecs) { + switch(video_codec) { + case GSR_VIDEO_CODEC_H264: + case GSR_VIDEO_CODEC_H264_VULKAN: { + if(use_software_video_encoder) + return (vec2i){4096, 2304}; + else if(supported_video_codecs->h264.supported) + return supported_video_codecs->h264.max_resolution; + break; + } + case GSR_VIDEO_CODEC_HEVC: + case GSR_VIDEO_CODEC_HEVC_VULKAN: { + if(supported_video_codecs->hevc.supported) + return supported_video_codecs->hevc.max_resolution; + break; + } + case GSR_VIDEO_CODEC_HEVC_HDR: + case GSR_VIDEO_CODEC_HEVC_HDR_VULKAN: { + if(supported_video_codecs->hevc_hdr.supported) + return supported_video_codecs->hevc_hdr.max_resolution; + break; + } + case GSR_VIDEO_CODEC_HEVC_10BIT: + case GSR_VIDEO_CODEC_HEVC_10BIT_VULKAN: { + if(supported_video_codecs->hevc_10bit.supported) + return supported_video_codecs->hevc_10bit.max_resolution; + break; + } + case GSR_VIDEO_CODEC_AV1: + case GSR_VIDEO_CODEC_AV1_VULKAN: { + if(supported_video_codecs->av1.supported) + return supported_video_codecs->av1.max_resolution; + break; + } + case GSR_VIDEO_CODEC_AV1_HDR: + case GSR_VIDEO_CODEC_AV1_HDR_VULKAN: { + if(supported_video_codecs->av1_hdr.supported) + return supported_video_codecs->av1_hdr.max_resolution; + break; + } + case GSR_VIDEO_CODEC_AV1_10BIT: + case GSR_VIDEO_CODEC_AV1_10BIT_VULKAN: { + if(supported_video_codecs->av1_10bit.supported) + return supported_video_codecs->av1_10bit.max_resolution; + break; + } + case GSR_VIDEO_CODEC_VP8: { + if(supported_video_codecs->vp8.supported) + return supported_video_codecs->vp8.max_resolution; + break; + } + case GSR_VIDEO_CODEC_VP9: { + if(supported_video_codecs->vp9.supported) + return supported_video_codecs->vp9.max_resolution; + break; + } + } + return (vec2i){0, 0}; +} + +bool codec_supports_resolution(vec2i codec_max_resolution, vec2i capture_resolution) { + if(codec_max_resolution.x == 0 || codec_max_resolution.y == 0) + return true; + return codec_max_resolution.x >= capture_resolution.x && codec_max_resolution.y >= capture_resolution.y; +} + +static void print_codec_error(gsr_video_codec video_codec) { + if(video_codec == (gsr_video_codec)GSR_VIDEO_CODEC_AUTO) + video_codec = GSR_VIDEO_CODEC_H264; + + const char *video_codec_name = video_codec_to_string(video_codec); + gsr_log(GSR_LOG_LEVEL_ERROR, "your gpu does not support '%s' video codec. If you are sure that your gpu does support '%s' video encoding and you are using an AMD/Intel GPU,\n then make sure you have installed the GPU specific vaapi packages (intel-media-driver, libva-intel-driver, libva-mesa-driver and linux-firmware).\n It's also possible that your distro has disabled hardware accelerated video encoding for '%s' video codec.\n This may be the case on corporate distros such as Manjaro, Fedora or OpenSUSE.\n You can test this by running 'vainfo | grep VAEntrypointEncSlice' to see if it matches any H264/HEVC/AV1/VP8/VP9 profile.\n On such distros, you need to manually install mesa from source to enable H264/HEVC hardware acceleration, or use a more user friendly distro. Alternatively record with AV1 if supported by your GPU.\n You can alternatively use the flatpak version of GPU Screen Recorder (https://flathub.org/apps/com.dec05eba.gpu_screen_recorder) which bypasses system issues with patented H264/HEVC codecs.\n If your GPU doesn't support hardware accelerated video encoding then you can use '-fallback-cpu-encoding yes' option to encode with your cpu instead.", video_codec_name, video_codec_name, video_codec_name); +} + +void force_cpu_encoding(gsr_recorder_settings *settings) { + settings->video_codec = GSR_VIDEO_CODEC_H264; + settings->video_encoder = GSR_VIDEO_ENCODER_HW_CPU; + if(settings->bitrate_mode == GSR_BITRATE_MODE_VBR) { + gsr_log(GSR_LOG_LEVEL_WARNING, "bitrate mode has been forcefully set to qp because software encoding option doesn't support vbr option"); + settings->bitrate_mode = GSR_BITRATE_MODE_QP; + } +} + +static int pick_video_codec(gsr_egl *egl, gsr_recorder_settings *settings, bool use_fallback_codec, bool *low_power, gsr_supported_video_codecs *supported_video_codecs, const AVCodec **video_codec) { + // TODO: software encoder for hevc, av1, vp8 and vp9 + *video_codec = NULL; + *low_power = false; + const AVCodec *video_codec_f = get_av_codec_if_supported(settings->video_codec, egl, settings->video_encoder == GSR_VIDEO_ENCODER_HW_CPU, supported_video_codecs); + + if(!video_codec_f && use_fallback_codec && settings->video_encoder != GSR_VIDEO_ENCODER_HW_CPU) { + switch(settings->video_codec) { + case GSR_VIDEO_CODEC_H264: { + gsr_log(GSR_LOG_LEVEL_ERROR, "selected video codec h264 is not supported by your hardware"); + if(settings->fallback_cpu_encoding) { + gsr_log(GSR_LOG_LEVEL_WARNING, "gpu encoding is not available on your system, trying cpu encoding instead because -fallback-cpu-encoding is enabled. Install the proper vaapi drivers on your system (if supported) if you experience performance issues"); + force_cpu_encoding(settings); + } + break; + } + case GSR_VIDEO_CODEC_HEVC: + case GSR_VIDEO_CODEC_HEVC_HDR: + case GSR_VIDEO_CODEC_HEVC_10BIT: { + gsr_log(GSR_LOG_LEVEL_WARNING, "selected video codec hevc is not supported by your hardware, trying h264 instead"); + settings->video_codec = GSR_VIDEO_CODEC_H264; + return pick_video_codec(egl, settings, true, low_power, supported_video_codecs, video_codec); + } + case GSR_VIDEO_CODEC_AV1: + case GSR_VIDEO_CODEC_AV1_HDR: + case GSR_VIDEO_CODEC_AV1_10BIT: { + gsr_log(GSR_LOG_LEVEL_WARNING, "selected video codec av1 is not supported by your hardware, trying h264 instead"); + settings->video_codec = GSR_VIDEO_CODEC_H264; + return pick_video_codec(egl, settings, true, low_power, supported_video_codecs, video_codec); + } + case GSR_VIDEO_CODEC_VP8: + case GSR_VIDEO_CODEC_VP9: + // TODO: Cant fallback to other codec because webm only supports vp8/vp9 + break; + case GSR_VIDEO_CODEC_H264_VULKAN: { + gsr_log(GSR_LOG_LEVEL_WARNING, "selected video codec h264_vulkan is not supported by your hardware, trying h264 instead"); + settings->video_codec = GSR_VIDEO_CODEC_H264; + // Need to do a query again because this time it's without vulkan + if(!get_supported_video_codecs(egl, settings->video_codec, false, true, supported_video_codecs)) { + gsr_log(GSR_LOG_LEVEL_ERROR, "failed to query for supported video codecs"); + print_codec_error(settings->video_codec); + return GSR_ERROR_VIDEO_CODEC_QUERY_FAILED; + } + return pick_video_codec(egl, settings, true, low_power, supported_video_codecs, video_codec); + } + case GSR_VIDEO_CODEC_HEVC_VULKAN: + case GSR_VIDEO_CODEC_HEVC_HDR_VULKAN: + case GSR_VIDEO_CODEC_HEVC_10BIT_VULKAN: { + gsr_log(GSR_LOG_LEVEL_WARNING, "selected video codec hevc_vulkan is not supported by your hardware, trying hevc instead"); + settings->video_codec = GSR_VIDEO_CODEC_HEVC; + // Need to do a query again because this time it's without vulkan + if(!get_supported_video_codecs(egl, settings->video_codec, false, true, supported_video_codecs)) { + gsr_log(GSR_LOG_LEVEL_ERROR, "failed to query for supported video codecs"); + print_codec_error(settings->video_codec); + return GSR_ERROR_VIDEO_CODEC_QUERY_FAILED; + } + return pick_video_codec(egl, settings, true, low_power, supported_video_codecs, video_codec); + } + case GSR_VIDEO_CODEC_AV1_VULKAN: + case GSR_VIDEO_CODEC_AV1_HDR_VULKAN: + case GSR_VIDEO_CODEC_AV1_10BIT_VULKAN: { + gsr_log(GSR_LOG_LEVEL_WARNING, "selected video codec av1_vulkan is not supported by your hardware, trying av1 instead"); + settings->video_codec = GSR_VIDEO_CODEC_AV1; + // Need to do a query again because this time it's without vulkan + if(!get_supported_video_codecs(egl, settings->video_codec, false, true, supported_video_codecs)) { + gsr_log(GSR_LOG_LEVEL_ERROR, "failed to query for supported video codecs"); + print_codec_error(settings->video_codec); + return GSR_ERROR_VIDEO_CODEC_QUERY_FAILED; + } + return pick_video_codec(egl, settings, true, low_power, supported_video_codecs, video_codec); + } + } + + video_codec_f = get_av_codec_if_supported(settings->video_codec, egl, settings->video_encoder == GSR_VIDEO_ENCODER_HW_CPU, supported_video_codecs); + } + + if(!video_codec_f) { + print_codec_error(settings->video_codec); + return GSR_ERROR_VIDEO_CODEC_UNSUPPORTED; + } + + *low_power = video_codec_only_supports_low_power_mode(supported_video_codecs, settings->video_codec); + *video_codec = video_codec_f; + return GSR_ERROR_OK; +} + +gsr_video_codec select_appropriate_video_codec_automatically(vec2i video_size, const gsr_supported_video_codecs *supported_video_codecs) { + if(supported_video_codecs->h264.supported && codec_supports_resolution(supported_video_codecs->h264.max_resolution, video_size)) { + gsr_log(GSR_LOG_LEVEL_INFO, "using h264 encoder because a codec was not specified"); + return GSR_VIDEO_CODEC_H264; + } else if(supported_video_codecs->hevc.supported && codec_supports_resolution(supported_video_codecs->hevc.max_resolution, video_size)) { + gsr_log(GSR_LOG_LEVEL_INFO, "using hevc encoder because a codec was not specified and h264 supported max resolution (%dx%d) is less than the capture resolution (%dx%d)", supported_video_codecs->h264.max_resolution.x, supported_video_codecs->h264.max_resolution.y, + video_size.x, video_size.y); + return GSR_VIDEO_CODEC_HEVC; + } else if(supported_video_codecs->av1.supported && codec_supports_resolution(supported_video_codecs->av1.max_resolution, video_size)) { + gsr_log(GSR_LOG_LEVEL_INFO, "using av1 encoder because a codec was not specified and hevc supported max resolution (%dx%d) is less than the capture resolution (%dx%d)", supported_video_codecs->hevc.max_resolution.x, supported_video_codecs->hevc.max_resolution.y, + video_size.x, video_size.y); + return GSR_VIDEO_CODEC_AV1; + } else { + return (gsr_video_codec)-1; + } +} + +int select_video_codec_with_fallback(vec2i video_size, gsr_recorder_settings *settings, const char *file_extension, gsr_egl *egl, bool *low_power, const AVCodec **video_codec) { + gsr_supported_video_codecs supported_video_codecs_non_vulkan; + get_supported_video_codecs(egl, settings->video_codec, settings->video_encoder == GSR_VIDEO_ENCODER_HW_CPU, true, &supported_video_codecs_non_vulkan); + + gsr_supported_video_codecs supported_video_codecs_vulkan = supported_video_codecs_non_vulkan; + set_supported_video_codecs_ffmpeg(&supported_video_codecs_non_vulkan, &supported_video_codecs_vulkan, egl->gpu_info.vendor); + + gsr_supported_video_codecs *supported_video_codecs = video_codec_is_vulkan(settings->video_codec) + ? &supported_video_codecs_vulkan + : &supported_video_codecs_non_vulkan; + + const bool video_codec_auto = settings->video_codec == (gsr_video_codec)GSR_VIDEO_CODEC_AUTO; + if(video_codec_auto) { + if(strcmp(file_extension, "webm") == 0) { + gsr_log(GSR_LOG_LEVEL_INFO, "using vp8 encoder because a codec was not specified and the file extension is .webm"); + settings->video_codec = GSR_VIDEO_CODEC_VP8; + } else if(settings->video_encoder == GSR_VIDEO_ENCODER_HW_CPU) { + gsr_log(GSR_LOG_LEVEL_INFO, "using h264 encoder because a codec was not specified"); + settings->video_codec = GSR_VIDEO_CODEC_H264; + } else if(settings->video_encoder != GSR_VIDEO_ENCODER_HW_CPU) { + settings->video_codec = select_appropriate_video_codec_automatically(video_size, &supported_video_codecs_non_vulkan); + if(settings->video_codec == (gsr_video_codec)-1) { + if(settings->fallback_cpu_encoding) { + gsr_log(GSR_LOG_LEVEL_WARNING, "gpu encoding is not available on your system or your gpu doesn't support recording at the resolution you are trying to record, trying cpu encoding instead because -fallback-cpu-encoding is enabled. Install the proper vaapi drivers on your system (if supported) if you experience performance issues"); + force_cpu_encoding(settings); + } else { + gsr_log(GSR_LOG_LEVEL_ERROR, "no video encoder was specified and neither h264, hevc nor av1 are supported on your system or you are trying to capture at a resolution higher than your system supports for each codec.\n" + " Ensure that you have installed the proper vaapi driver. If your gpu doesn't support video encoding then you can run gpu-screen-recorder with \"-fallback-cpu-encoding yes\" option to use cpu encoding."); + return GSR_ERROR_NO_VIDEO_CODEC_AVAILABLE; + } + } + } + } + + if(LIBAVFORMAT_VERSION_INT < AV_VERSION_INT(60, 10, 100) && strcmp(file_extension, "flv") == 0) { + if(settings->video_codec != GSR_VIDEO_CODEC_H264) { + settings->video_codec = GSR_VIDEO_CODEC_H264; + gsr_log(GSR_LOG_LEVEL_WARNING, "hevc/av1 is not compatible with flv in your outdated version of ffmpeg, falling back to h264 instead."); + } + } else if(strcmp(file_extension, "m3u8") == 0) { + if(video_codec_is_av1(settings->video_codec)) { + settings->video_codec = GSR_VIDEO_CODEC_HEVC; + gsr_log(GSR_LOG_LEVEL_WARNING, "av1 is not compatible with hls (m3u8), falling back to hevc instead."); + } + } + + const AVCodec *codec = NULL; + const int pick_codec_result = pick_video_codec(egl, settings, true, low_power, supported_video_codecs, &codec); + if(pick_codec_result != GSR_ERROR_OK) + return pick_codec_result; + + const vec2i codec_max_resolution = codec_get_max_resolution(settings->video_codec, settings->video_encoder == GSR_VIDEO_ENCODER_HW_CPU, supported_video_codecs); + if(!codec_supports_resolution(codec_max_resolution, video_size)) { + const char *video_codec_name = video_codec_to_string(settings->video_codec); + gsr_log(GSR_LOG_LEVEL_ERROR, "The max resolution for video codec %s is %dx%d while you are trying to capture at resolution %dx%d. Change capture resolution or video codec and try again", video_codec_name, codec_max_resolution.x, codec_max_resolution.y, video_size.x, video_size.y); + return GSR_ERROR_VIDEO_CODEC_RESOLUTION_UNSUPPORTED; + } + + *video_codec = codec; + return GSR_ERROR_OK; +} -- cgit v1.2.3 From cdb8493eed42c386919280b17bbd31b713a5ac35 Mon Sep 17 00:00:00 2001 From: dec05eba Date: Sat, 1 Aug 2026 21:22:04 +0200 Subject: Move windowing setup from main.cpp into windowing module The recording path and the list commands now share the same windowing setup code instead of duplicating it. --- include/recorder/windowing.h | 34 +++++++ meson.build | 1 + src/main.cpp | 210 ++++++++++--------------------------------- src/recorder/windowing.c | 122 +++++++++++++++++++++++++ 4 files changed, 202 insertions(+), 165 deletions(-) create mode 100644 include/recorder/windowing.h create mode 100644 src/recorder/windowing.c diff --git a/include/recorder/windowing.h b/include/recorder/windowing.h new file mode 100644 index 0000000..6bc5f5f --- /dev/null +++ b/include/recorder/windowing.h @@ -0,0 +1,34 @@ +#ifndef GSR_RECORDER_WINDOWING_H +#define GSR_RECORDER_WINDOWING_H + +#include +#include "../defs.h" +#include "../egl.h" + +#include + +typedef struct { + bool monitor_capture; + bool gl_debug; + bool listen_to_x11_events; +} gsr_windowing_params; + +typedef struct { + Display *display; + gsr_window *window; + gsr_egl egl; + bool egl_loaded; + bool card_path_found; +} gsr_windowing; + +/* Returns a |gsr_error| value. Connects to the display server and creates a window */ +int gsr_windowing_init(gsr_windowing *self, const gsr_windowing_params *params); +/* Returns a |gsr_error| value. Loads opengl and finds the drm card to use */ +int gsr_windowing_load_egl(gsr_windowing *self, const gsr_windowing_params *params); +bool gsr_windowing_is_wayland(const gsr_windowing *self); + +bool gsr_windowing_is_using_prime_run(void); +void gsr_windowing_disable_prime_run(void); +bool monitor_capture_use_drm(const gsr_window *window, gsr_gpu_vendor vendor); + +#endif /* GSR_RECORDER_WINDOWING_H */ diff --git a/meson.build b/meson.build index de7d2f2..2568340 100644 --- a/meson.build +++ b/meson.build @@ -35,6 +35,7 @@ src = [ 'src/recorder/audio_codec.c', 'src/recorder/video_codec.c', 'src/recorder/codec_select.c', + 'src/recorder/windowing.c', 'src/egl.c', 'src/cuda.c', 'src/window_texture.c', diff --git a/src/main.cpp b/src/main.cpp index 70ef9e8..ec5a260 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -3,6 +3,7 @@ extern "C" { #include "../include/recorder/audio_codec.h" #include "../include/recorder/video_codec.h" #include "../include/recorder/codec_select.h" +#include "../include/recorder/windowing.h" #include "../include/recorder/error.h" #include "../include/capture/nvfbc.h" #include "../include/capture/xcomposite.h" @@ -151,14 +152,6 @@ static void get_monitor_by_position_callback(const gsr_monitor *monitor, void *u } } -static int x11_error_handler(Display*, XErrorEvent*) { - return 0; -} - -static int x11_io_error_handler(Display*) { - return 0; -} - /* AV1/VP9 use the 0-255 qindex quantizer scale and VP8 uses 0-127. Unlike h264 qp the quantizer step size is not exponential in qindex, so the mapping to an equivalent h264 qp is not linear in the quantizer range. qindex = h264 qp * 4 matches the qindex = crf * 4 scale used by libvpx/libaom, where crf is roughly equivalent to h264 qp. */ @@ -813,44 +806,6 @@ fail: return err; } -static void xwayland_check_callback(const gsr_monitor *monitor, void *userdata) { - bool *xwayland_found = (bool*)userdata; - if(monitor->name_len >= 8 && strncmp(monitor->name, "XWAYLAND", 8) == 0) - *xwayland_found = true; - else if(memmem(monitor->name, monitor->name_len, "X11", 3)) - *xwayland_found = true; -} - -static bool is_xwayland(Display *display) { - int opcode, event, error; - if(XQueryExtension(display, "XWAYLAND", &opcode, &event, &error)) - return true; - - bool xwayland_found = false; - for_each_active_monitor_output_x11_not_cached(display, xwayland_check_callback, &xwayland_found); - return xwayland_found; -} - -static bool is_using_prime_run() { - const char *prime_render_offload = getenv("__NV_PRIME_RENDER_OFFLOAD"); - return (prime_render_offload && strcmp(prime_render_offload, "1") == 0) || getenv("DRI_PRIME"); -} - -static void disable_prime_run() { - unsetenv("__NV_PRIME_RENDER_OFFLOAD"); - unsetenv("__NV_PRIME_RENDER_OFFLOAD_PROVIDER"); - unsetenv("__GLX_VENDOR_LIBRARY_NAME"); - unsetenv("__VK_LAYER_NV_optimus"); - unsetenv("DRI_PRIME"); -} - -static gsr_window* gsr_window_create(Display *display, bool wayland) { - if(wayland) - return gsr_window_wayland_create(); - else - return gsr_window_x11_create(display); -} - static void list_system_info(bool wayland) { printf("display_server|%s\n", wayland ? "wayland" : "x11"); bool supports_app_audio = false; @@ -931,10 +886,6 @@ static void list_supported_video_codecs(gsr_egl *egl, bool wayland) { puts("av1_10bit_vulkan"); } -static bool monitor_capture_use_drm(const gsr_window *window, gsr_gpu_vendor vendor) { - return gsr_window_get_display_server(window) == GSR_DISPLAY_SERVER_WAYLAND || vendor != GSR_GPU_VENDOR_NVIDIA; -} - typedef struct { const gsr_window *window; int num_monitors; @@ -1014,90 +965,34 @@ static void version_command(void *userdata) { _exit(0); } -struct WindowingSetup { - Display *dpy; - gsr_window *window; - gsr_egl egl; - bool list_monitors; -}; - -static WindowingSetup setup_windowing(bool setup_egl) { - WindowingSetup setup; - memset(&setup, 0, sizeof(setup)); - - bool wayland = false; - setup.dpy = XOpenDisplay(nullptr); - if (!setup.dpy) { - wayland = true; - gsr_log(GSR_LOG_LEVEL_WARNING, "failed to connect to the X server. Assuming wayland is running without Xwayland"); - } - - XSetErrorHandler(x11_error_handler); - XSetIOErrorHandler(x11_io_error_handler); - - if(!wayland) - wayland = is_xwayland(setup.dpy); - - if(!wayland && is_using_prime_run()) { - // Disable prime-run and similar options as it doesn't work, the monitor to capture has to be run on the same device. - // This is fine on wayland since nvidia uses drm interface there and the monitor query checks the monitors connected - // to the drm device. - gsr_log(GSR_LOG_LEVEL_WARNING, "use of prime-run on X11 is not supported. Disabling prime-run"); - disable_prime_run(); - } - - setup.window = gsr_window_create(setup.dpy, wayland); - if(!setup.window) { - gsr_log(GSR_LOG_LEVEL_ERROR, "failed to create window"); - _exit(1); - } - - setup.list_monitors = true; - - if(setup_egl) { - if(!gsr_egl_load(&setup.egl, setup.window, false, false)) { - gsr_log(GSR_LOG_LEVEL_ERROR, "failed to load opengl"); - _exit(22); - } - - setup.egl.card_path[0] = '\0'; - if(monitor_capture_use_drm(setup.window, setup.egl.gpu_info.vendor)) { - // TODO: Allow specifying another card, and in other places - if(!gsr_get_valid_card_path(&setup.egl, setup.egl.card_path, true)) { - gsr_log(GSR_LOG_LEVEL_ERROR, "no /dev/dri/cardX device found. Make sure that you have at least one monitor connected"); - setup.list_monitors = false; - } - } else { - gsr_get_valid_card_path(&setup.egl, setup.egl.card_path, false); - } - } - - return setup; -} - static void info_command(void *userdata) { (void)userdata; - WindowingSetup windowing_setup = setup_windowing(true); - const bool wayland = gsr_window_get_display_server(windowing_setup.window) == GSR_DISPLAY_SERVER_WAYLAND; + gsr_windowing windowing; + const gsr_windowing_params windowing_params = {false, false, false}; + if(gsr_windowing_init(&windowing, &windowing_params) != GSR_ERROR_OK) + _exit(1); + if(gsr_windowing_load_egl(&windowing, &windowing_params) != GSR_ERROR_OK) + _exit(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_setup.egl.gpu_info.is_steam_deck) + 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_setup.egl); + list_gpu_info(&windowing.egl); puts("section=video_codecs"); - list_supported_video_codecs(&windowing_setup.egl, wayland); + list_supported_video_codecs(&windowing.egl, wayland); puts("section=image_formats"); puts("jpeg"); puts("png"); puts("section=capture_options"); - list_supported_capture_options(windowing_setup.window, windowing_setup.egl.card_path, windowing_setup.list_monitors); + list_supported_capture_options(windowing.window, windowing.egl.card_path, windowing.card_path_found); fflush(stdout); @@ -1162,12 +1057,17 @@ static void list_v4l2_devices(void *userdata) { // |card_path| can be NULL. If not NULL then |vendor| has to be valid static void list_capture_options_command(const char *card_path, void *userdata) { (void)userdata; - WindowingSetup windowing_setup = setup_windowing(card_path != nullptr); + gsr_windowing windowing; + const gsr_windowing_params windowing_params = {false, false, false}; + if(gsr_windowing_init(&windowing, &windowing_params) != GSR_ERROR_OK) + _exit(1); + if(!card_path && gsr_windowing_load_egl(&windowing, &windowing_params) != GSR_ERROR_OK) + _exit(22); if(card_path) - list_supported_capture_options(windowing_setup.window, card_path, true); + list_supported_capture_options(windowing.window, card_path, true); else - list_supported_capture_options(windowing_setup.window, windowing_setup.egl.card_path, windowing_setup.list_monitors); + list_supported_capture_options(windowing.window, windowing.egl.card_path, windowing.card_path_found); fflush(stdout); @@ -1182,10 +1082,15 @@ static void list_capture_options_command(const char *card_path, void *userdata) static void list_monitors_command(void *userdata) { (void)userdata; - WindowingSetup windowing_setup = setup_windowing(true); + gsr_windowing windowing; + const gsr_windowing_params windowing_params = {false, false, false}; + if(gsr_windowing_init(&windowing, &windowing_params) != GSR_ERROR_OK) + _exit(1); + if(gsr_windowing_load_egl(&windowing, &windowing_params) != GSR_ERROR_OK) + _exit(22); - if(windowing_setup.list_monitors) - list_monitors(windowing_setup.window, windowing_setup.egl.card_path); + if(windowing.card_path_found) + list_monitors(windowing.window, windowing.egl.card_path); fflush(stdout); @@ -2600,39 +2505,21 @@ int main(int argc, char **argv) { validate_merged_audio_inputs_app_audio(requested_audio_inputs, app_audio_names); - bool wayland = false; - Display *dpy = XOpenDisplay(nullptr); - if(dpy) { - XSelectInput(dpy, DefaultRootWindow(dpy), PropertyChangeMask); - } else { - wayland = true; - gsr_log(GSR_LOG_LEVEL_WARNING, "failed to connect to the X server. Assuming wayland is running without Xwayland"); - } - - XSetErrorHandler(x11_error_handler); - XSetIOErrorHandler(x11_io_error_handler); - - if(!wayland) - wayland = is_xwayland(dpy); - - if(!wayland && is_using_prime_run()) { - // Disable prime-run and similar options as it doesn't work, the monitor to capture has to be run on the same device. - // This is fine on wayland since nvidia uses drm interface there and the monitor query checks the monitors connected - // to the drm device. - gsr_log(GSR_LOG_LEVEL_WARNING, "use of prime-run on X11 is not supported. Disabling prime-run"); - disable_prime_run(); - } - - gsr_window *window = gsr_window_create(dpy, wayland); - if(!window) { - gsr_log(GSR_LOG_LEVEL_ERROR, "failed to create window"); + gsr_windowing windowing; + gsr_windowing_params windowing_params; + windowing_params.monitor_capture = is_capturing_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(1); - } + + Display *dpy = windowing.display; + gsr_window *window = windowing.window; if(is_capturing_type(capture_sources, GSR_CAPTURE_SOURCE_TYPE_PORTAL)) { - if(is_using_prime_run()) { + 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"); - disable_prime_run(); + gsr_windowing_disable_prime_run(); } if(video_codec_is_hdr(arg_parser.settings.video_codec)) { @@ -2641,11 +2528,10 @@ int main(int argc, char **argv) { } } - gsr_egl egl; - if(!gsr_egl_load(&egl, window, is_capturing_monitor_or_region(capture_sources), arg_parser.settings.gl_debug)) { - gsr_log(GSR_LOG_LEVEL_ERROR, "failed to load opengl"); + if(gsr_windowing_load_egl(&windowing, &windowing_params) != GSR_ERROR_OK) _exit(1); - } + + gsr_egl &egl = windowing.egl; gsr_shader_enable_debug_output(arg_parser.settings.gl_debug); #ifndef NDEBUG @@ -2655,15 +2541,9 @@ int main(int argc, char **argv) { if(!args_parser_validate_with_gl_info(&arg_parser, &egl)) _exit(1); - egl.card_path[0] = '\0'; - if(monitor_capture_use_drm(window, egl.gpu_info.vendor)) { - // TODO: Allow specifying another card, and in other places - if(!gsr_get_valid_card_path(&egl, egl.card_path, is_capturing_monitor_or_region(capture_sources))) { - 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(2); - } - } else { - gsr_get_valid_card_path(&egl, egl.card_path, false); + 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(2); } memset(&x11_cursor, 0, sizeof(x11_cursor)); diff --git a/src/recorder/windowing.c b/src/recorder/windowing.c new file mode 100644 index 0000000..6671898 --- /dev/null +++ b/src/recorder/windowing.c @@ -0,0 +1,122 @@ +#include "../../include/recorder/windowing.h" +#include "../../include/recorder/error.h" +#include "../../include/window/x11.h" +#include "../../include/window/wayland.h" +#include "../../include/utils.h" +#include "../../include/log.h" + +#include +#include + +static int x11_error_handler(Display *display, XErrorEvent *event) { + (void)display; + (void)event; + return 0; +} + +static int x11_io_error_handler(Display *display) { + (void)display; + return 0; +} + +static void xwayland_check_callback(const gsr_monitor *monitor, void *userdata) { + bool *xwayland_found = (bool*)userdata; + if(monitor->name_len >= 8 && strncmp(monitor->name, "XWAYLAND", 8) == 0) + *xwayland_found = true; + else if(memmem(monitor->name, monitor->name_len, "X11", 3)) + *xwayland_found = true; +} + +static bool is_xwayland(Display *display) { + int opcode, event, error; + if(XQueryExtension(display, "XWAYLAND", &opcode, &event, &error)) + return true; + + bool xwayland_found = false; + for_each_active_monitor_output_x11_not_cached(display, xwayland_check_callback, &xwayland_found); + return xwayland_found; +} + +bool gsr_windowing_is_using_prime_run(void) { + const char *prime_render_offload = getenv("__NV_PRIME_RENDER_OFFLOAD"); + return (prime_render_offload && strcmp(prime_render_offload, "1") == 0) || getenv("DRI_PRIME"); +} + +void gsr_windowing_disable_prime_run(void) { + unsetenv("__NV_PRIME_RENDER_OFFLOAD"); + unsetenv("__NV_PRIME_RENDER_OFFLOAD_PROVIDER"); + unsetenv("__GLX_VENDOR_LIBRARY_NAME"); + unsetenv("__VK_LAYER_NV_optimus"); + unsetenv("DRI_PRIME"); +} + +static gsr_window* window_create(Display *display, bool wayland) { + if(wayland) + return gsr_window_wayland_create(); + else + return gsr_window_x11_create(display); +} + +bool monitor_capture_use_drm(const gsr_window *window, gsr_gpu_vendor vendor) { + return gsr_window_get_display_server(window) == GSR_DISPLAY_SERVER_WAYLAND || vendor != GSR_GPU_VENDOR_NVIDIA; +} + +int gsr_windowing_init(gsr_windowing *self, const gsr_windowing_params *params) { + memset(self, 0, sizeof(*self)); + self->card_path_found = true; + + bool wayland = false; + self->display = XOpenDisplay(NULL); + if(self->display) { + if(params->listen_to_x11_events) + XSelectInput(self->display, DefaultRootWindow(self->display), PropertyChangeMask); + } else { + wayland = true; + gsr_log(GSR_LOG_LEVEL_WARNING, "failed to connect to the X server. Assuming wayland is running without Xwayland"); + } + + XSetErrorHandler(x11_error_handler); + XSetIOErrorHandler(x11_io_error_handler); + + if(!wayland) + wayland = is_xwayland(self->display); + + if(!wayland && gsr_windowing_is_using_prime_run()) { + // Disable prime-run and similar options as it doesn't work, the monitor to capture has to be run on the same device. + // This is fine on wayland since nvidia uses drm interface there and the monitor query checks the monitors connected + // to the drm device. + gsr_log(GSR_LOG_LEVEL_WARNING, "use of prime-run on X11 is not supported. Disabling prime-run"); + gsr_windowing_disable_prime_run(); + } + + self->window = window_create(self->display, wayland); + if(!self->window) { + gsr_log(GSR_LOG_LEVEL_ERROR, "failed to create window"); + return GSR_ERROR_GENERIC; + } + + return GSR_ERROR_OK; +} + +int gsr_windowing_load_egl(gsr_windowing *self, const gsr_windowing_params *params) { + if(!gsr_egl_load(&self->egl, self->window, params->monitor_capture, params->gl_debug)) { + gsr_log(GSR_LOG_LEVEL_ERROR, "failed to load opengl"); + return GSR_ERROR_OPENGL_LOAD_FAILED; + } + self->egl_loaded = true; + + self->egl.card_path[0] = '\0'; + if(monitor_capture_use_drm(self->window, self->egl.gpu_info.vendor)) { + // TODO: Allow specifying another card, and in other places + if(!gsr_get_valid_card_path(&self->egl, self->egl.card_path, params->monitor_capture)) + self->card_path_found = false; + } else { + gsr_get_valid_card_path(&self->egl, self->egl.card_path, false); + } + + return GSR_ERROR_OK; +} + +bool gsr_windowing_is_wayland(const gsr_windowing *self) { + return gsr_window_get_display_server(self->window) == GSR_DISPLAY_SERVER_WAYLAND; +} -- cgit v1.2.3 From de8954256578e1805fc5b92a0719190de784d3c0 Mon Sep 17 00:00:00 2001 From: dec05eba Date: Sat, 1 Aug 2026 21:32:06 +0200 Subject: Move capture source parsing and capture creation from main.cpp into C modules Capture sources are C structs with fixed size names and a growable array, and the lazily initialized kms client, night light and cursor globals are now owned by gsr_capture_deps. --- include/recorder/capture_setup.h | 49 ++ include/recorder/capture_source.h | 60 +++ include/recorder/error.h | 2 +- meson.build | 2 + src/main.cpp | 929 ++++---------------------------------- src/recorder/capture_setup.c | 543 ++++++++++++++++++++++ src/recorder/capture_source.c | 390 ++++++++++++++++ 7 files changed, 1128 insertions(+), 847 deletions(-) create mode 100644 include/recorder/capture_setup.h create mode 100644 include/recorder/capture_source.h create mode 100644 src/recorder/capture_setup.c create mode 100644 src/recorder/capture_source.c diff --git a/include/recorder/capture_setup.h b/include/recorder/capture_setup.h new file mode 100644 index 0000000..fd4b7e7 --- /dev/null +++ b/include/recorder/capture_setup.h @@ -0,0 +1,49 @@ +#ifndef GSR_RECORDER_CAPTURE_SETUP_H +#define GSR_RECORDER_CAPTURE_SETUP_H + +#include +#include +#include "../egl.h" +#include "../cursor.h" +#include "../kde_night_light.h" +#include "../capture/capture.h" +#include "../../kms/client/kms_client.h" +#include "capture_source.h" +#include "settings.h" + +#include + +typedef struct { + gsr_kms_client kms_client; + bool kms_client_initialized; + gsr_kms_response kms_response; + gsr_kde_night_light *kde_night_light; + bool kde_night_light_initialized; + gsr_cursor x11_cursor; + Display *x11_cursor_display; +} gsr_capture_deps; + +typedef struct { + gsr_capture *capture; + gsr_capture_metadata metadata; + gsr_capture_source *capture_source; +} gsr_video_source; + +typedef struct { + gsr_video_source *items; + size_t num_items; +} gsr_video_sources; + +void gsr_capture_deps_init(gsr_capture_deps *self); +void gsr_capture_deps_init_cursor(gsr_capture_deps *self, gsr_egl *egl, bool record_cursor); +void gsr_capture_deps_deinit(gsr_capture_deps *self); +void gsr_capture_deps_cleanup_kms_fds(gsr_capture_deps *self); +void gsr_capture_deps_update_kms(gsr_capture_deps *self); + +/* Returns a |gsr_error| value, or the error returned by gsr_capture_start. |video_size| is set to the size of all capture sources combined */ +int gsr_video_sources_create(gsr_video_sources *self, const gsr_recorder_settings *settings, gsr_egl *egl, gsr_capture_deps *deps, bool prefer_ximage, gsr_capture_sources *capture_sources, vec2i *video_size); +void gsr_video_sources_update_with_real_video_size(gsr_video_sources *self, vec2i video_size); +bool gsr_video_sources_uses_external_image(const gsr_video_sources *self); +void gsr_video_sources_deinit(gsr_video_sources *self); + +#endif /* GSR_RECORDER_CAPTURE_SETUP_H */ diff --git a/include/recorder/capture_source.h b/include/recorder/capture_source.h new file mode 100644 index 0000000..503c620 --- /dev/null +++ b/include/recorder/capture_source.h @@ -0,0 +1,60 @@ +#ifndef GSR_RECORDER_CAPTURE_SOURCE_H +#define GSR_RECORDER_CAPTURE_SOURCE_H + +#include +#include +#include +#include "../vec2.h" +#include "../args_parser.h" +#include "../capture/capture.h" +#include "../capture/v4l2.h" + +#define GSR_CAPTURE_SOURCE_NAME_MAX_SIZE 256 + +typedef enum { + VVEC2I_TYPE_PIXELS, + VVEC2I_TYPE_SCALAR +} vvec2i_type; + +typedef struct { + int x; + int y; + vvec2i_type x_type; + vvec2i_type y_type; +} vvec2i; + +typedef struct { + char name[GSR_CAPTURE_SOURCE_NAME_MAX_SIZE]; + CaptureSourceType type; + gsr_capture_alignment halign; + gsr_capture_alignment valign; + gsr_capture_v4l2_pixfmt v4l2_pixfmt; + uint32_t flip; + vvec2i pos; + vvec2i size; + vec2i region_pos; + vec2i region_size; + bool region_set; + int64_t window_id; + int camera_fps; + vec2i camera_resolution; +} gsr_capture_source; + +typedef struct { + gsr_capture_source *items; + size_t num_items; + size_t capacity_items; +} gsr_capture_sources; + +void gsr_capture_source_init(gsr_capture_source *self, vec2i region_position, vec2i region_size); + +/* Returns a |gsr_error| value. Parses the -w option value, which may contain multiple capture sources separated by | */ +int gsr_capture_sources_parse(gsr_capture_sources *self, const char *capture_source_arg, vec2i region_position, vec2i region_size); +void gsr_capture_sources_deinit(gsr_capture_sources *self); + +bool gsr_capture_sources_has_type(const gsr_capture_sources *self, CaptureSourceType type); +bool gsr_capture_sources_has_damage_tracked_target(const gsr_capture_sources *self); +bool gsr_capture_sources_has_region_set(const gsr_capture_sources *self); +bool gsr_capture_sources_has_monitor_or_region(const gsr_capture_sources *self); + +#endif /* GSR_RECORDER_CAPTURE_SOURCE_H */ diff --git a/include/recorder/error.h b/include/recorder/error.h index 75f1416..cfa83cd 100644 --- a/include/recorder/error.h +++ b/include/recorder/error.h @@ -9,7 +9,7 @@ extern "C" { typedef enum { GSR_ERROR_OK = 0, GSR_ERROR_GENERIC = -1, - GSR_ERROR_AUDIO_SETUP_FAILED = -2, + GSR_ERROR_UNSUPPORTED = -2, GSR_ERROR_CAPTURE_FAILED = -3, GSR_ERROR_VIDEO_CODEC_QUERY_FAILED = -11, GSR_ERROR_OPENGL_LOAD_FAILED = -22, diff --git a/meson.build b/meson.build index 2568340..af5473d 100644 --- a/meson.build +++ b/meson.build @@ -36,6 +36,8 @@ src = [ 'src/recorder/video_codec.c', 'src/recorder/codec_select.c', 'src/recorder/windowing.c', + 'src/recorder/capture_source.c', + 'src/recorder/capture_setup.c', 'src/egl.c', 'src/cuda.c', 'src/window_texture.c', diff --git a/src/main.cpp b/src/main.cpp index ec5a260..d64be78 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -4,6 +4,8 @@ extern "C" { #include "../include/recorder/video_codec.h" #include "../include/recorder/codec_select.h" #include "../include/recorder/windowing.h" +#include "../include/recorder/capture_source.h" +#include "../include/recorder/capture_setup.h" #include "../include/recorder/error.h" #include "../include/capture/nvfbc.h" #include "../include/capture/xcomposite.h" @@ -99,59 +101,6 @@ struct MergedAudioInputs { static const int VIDEO_STREAM_INDEX = 0; -typedef struct { - const gsr_window *window; -} MonitorOutputCallbackUserdata; - -static void monitor_output_callback_print(const gsr_monitor *monitor, void *userdata) { - const MonitorOutputCallbackUserdata *options = (MonitorOutputCallbackUserdata*)userdata; - vec2i monitor_position = monitor->pos; - vec2i monitor_size = monitor->size; - if(gsr_window_get_display_server(options->window) == GSR_DISPLAY_SERVER_WAYLAND) { - gsr_monitor_rotation monitor_rotation = GSR_MONITOR_ROT_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) - std::swap(monitor_size.x, monitor_size.y); - } - fprintf(stderr, " \"%.*s\" (%dx%d+%d+%d)\n", monitor->name_len, monitor->name, monitor_size.x, monitor_size.y, monitor_position.x, monitor_position.y); -} - -typedef struct { - char *output_name; -} FirstOutputCallback; - -static void get_first_output_callback(const gsr_monitor *monitor, void *userdata) { - FirstOutputCallback *data = (FirstOutputCallback*)userdata; - if(!data->output_name) - data->output_name = strdup(monitor->name); -} - -typedef struct { - gsr_window *window; - vec2i position; - char *output_name; - vec2i monitor_pos; - vec2i monitor_size; - double monitor_scale_inverted; -} MonitorByPositionCallback; - -static void get_monitor_by_position_callback(const gsr_monitor *monitor, void *userdata) { - MonitorByPositionCallback *data = (MonitorByPositionCallback*)userdata; - - const vec2i monitor_position = monitor->logical_pos; - const vec2i monitor_size = monitor->size; - const vec2i monitor_logical_size = monitor->logical_size; - - if(!data->output_name && data->position.x >= monitor_position.x && data->position.x <= monitor_position.x + monitor_logical_size.x - && data->position.y >= monitor_position.y && data->position.y <= monitor_position.y + monitor_logical_size.y) - { - data->output_name = strdup(monitor->name); - data->monitor_pos = monitor_position; - data->monitor_size = monitor_size; - data->monitor_scale_inverted = (double)monitor_size.x / (double)monitor_logical_size.x; - } -} - /* AV1/VP9 use the 0-255 qindex quantizer scale and VP8 uses 0-127. Unlike h264 qp the quantizer step size is not exponential in qindex, so the mapping to an equivalent h264 qp is not linear in the quantizer range. qindex = h264 qp * 4 matches the qindex = crf * 4 scale used by libvpx/libaom, where crf is roughly equivalent to h264 qp. */ @@ -363,41 +312,7 @@ struct RecordingStartResult { std::vector audio_inputs; }; -typedef enum { - VVEC2I_TYPE_PIXELS, - VVEC2I_TYPE_SCALAR -} vvec2i_type; - -typedef struct { - int x, y; - vvec2i_type x_type; - vvec2i_type y_type; -} vvec2i; - -struct CaptureSource { - std::string name; - CaptureSourceType type = GSR_CAPTURE_SOURCE_TYPE_WINDOW; - gsr_capture_alignment halign = GSR_CAPTURE_ALIGN_CENTER; - gsr_capture_alignment valign = GSR_CAPTURE_ALIGN_CENTER; - gsr_capture_v4l2_pixfmt v4l2_pixfmt = GSR_CAPTURE_V4L2_PIXFMT_AUTO; - uint32_t flip = GSR_FLIP_NONE; - vvec2i pos = {0, 0, VVEC2I_TYPE_PIXELS, VVEC2I_TYPE_PIXELS}; - vvec2i size = {100, 100, VVEC2I_TYPE_SCALAR, VVEC2I_TYPE_SCALAR}; - vec2i region_pos = {0, 0}; - vec2i region_size = {0, 0}; - bool region_set = false; - int64_t window_id = 0; - int camera_fps = 0; - vec2i camera_resolution = {0, 0}; -}; - -struct VideoSource { - gsr_capture *capture; - gsr_capture_metadata metadata; - CaptureSource *capture_source; -}; - -static RecordingStartResult start_recording_create_streams(const char *filename, const args_parser &arg_parser, AVCodecContext *video_codec_context, const std::vector &audio_tracks, bool hdr, std::vector &video_sources) { +static RecordingStartResult start_recording_create_streams(const char *filename, const args_parser &arg_parser, AVCodecContext *video_codec_context, const std::vector &audio_tracks, bool hdr, gsr_video_sources *video_sources) { AVFormatContext *av_format_context; avformat_alloc_output_context2(&av_format_context, nullptr, arg_parser.settings.container_format, filename); set_format_context_options(av_format_context); @@ -437,7 +352,8 @@ static RecordingStartResult start_recording_create_streams(const char *filename, return result; } - for(VideoSource &video_source : video_sources) { + for(size_t video_source_index = 0; video_source_index < video_sources->num_items; ++video_source_index) { + gsr_video_source &video_source = video_sources->items[video_source_index]; if(hdr && add_hdr_metadata_to_video_stream(video_source.capture, video_stream)) break; } @@ -494,7 +410,7 @@ struct AudioPtsOffset { int stream_index = 0; }; -static bool save_replay_async(AVCodecContext *video_codec_context, int video_stream_index, const std::vector &audio_tracks, gsr_encoder *encoder, const args_parser &arg_parser, const std::string &file_extension, bool date_folders, bool hdr, std::vector &video_sources, int current_save_replay_seconds) { +static bool save_replay_async(AVCodecContext *video_codec_context, int video_stream_index, const std::vector &audio_tracks, gsr_encoder *encoder, const args_parser &arg_parser, const std::string &file_extension, bool date_folders, bool hdr, gsr_video_sources *video_sources, int current_save_replay_seconds) { if(save_replay_thread.valid()) return true; @@ -1103,262 +1019,7 @@ static void list_monitors_command(void *userdata) { _exit(0); } -static std::string validate_monitor_get_valid(const gsr_egl *egl, const char* window) { - const bool is_x11 = gsr_window_get_display_server(egl->window) == GSR_DISPLAY_SERVER_X11; - const gsr_connection_type connection_type = is_x11 ? GSR_CONNECTION_X11 : GSR_CONNECTION_DRM; - const bool capture_use_drm = monitor_capture_use_drm(egl->window, egl->gpu_info.vendor); - - std::string capture_source_result = window; - if(strcmp(capture_source_result.c_str(), "screen") == 0) { - FirstOutputCallback data; - data.output_name = NULL; - for_each_active_monitor_output(egl->window, egl->card_path, connection_type, get_first_output_callback, &data); - - if(data.output_name) { - capture_source_result = data.output_name; - free(data.output_name); - } else { - gsr_log(GSR_LOG_LEVEL_ERROR, "no usable output found"); - _exit(51); - } - } else if(capture_use_drm || (strcmp(capture_source_result.c_str(), "screen-direct") != 0 && strcmp(capture_source_result.c_str(), "screen-direct-force") != 0)) { - gsr_monitor gmon; - if(!get_monitor_by_name(egl, connection_type, capture_source_result.c_str(), &gmon)) { - gsr_log(GSR_LOG_LEVEL_ERROR, "display \"%s\" not found, expected one of:", capture_source_result.c_str()); - fprintf(stderr, " \"screen\"\n"); - if(!capture_use_drm) - fprintf(stderr, " \"screen-direct\"\n"); - - MonitorOutputCallbackUserdata userdata; - userdata.window = egl->window; - for_each_active_monitor_output(egl->window, egl->card_path, connection_type, monitor_output_callback_print, &userdata); - _exit(51); - } - } - return capture_source_result; -} - -static std::string get_monitor_by_region_center(const gsr_egl *egl, vec2i region_position, vec2i region_size, vec2i *monitor_pos, vec2i *monitor_size, double *monitor_scale_inverted) { - const bool is_x11 = gsr_window_get_display_server(egl->window) == GSR_DISPLAY_SERVER_X11; - const gsr_connection_type connection_type = is_x11 ? GSR_CONNECTION_X11 : GSR_CONNECTION_WAYLAND; - - MonitorByPositionCallback data; - data.window = egl->window; - data.position = { region_position.x + region_size.x / 2, region_position.y + region_size.y / 2 }; - data.output_name = NULL; - data.monitor_pos = {0, 0}; - data.monitor_size = {0, 0}; - data.monitor_scale_inverted = 1.0; - for_each_active_monitor_output(egl->window, egl->card_path, connection_type, get_monitor_by_position_callback, &data); - - std::string result; - if(data.output_name) { - result = data.output_name; - free(data.output_name); - } - *monitor_pos = data.monitor_pos; - *monitor_size = data.monitor_size; - *monitor_scale_inverted = data.monitor_scale_inverted; - return result; -} - -static gsr_kms_client kms_client; -static bool kms_client_initialized = false; -static gsr_kms_response kms_response; -static gsr_kde_night_light *kde_night_light = nullptr; -static bool kde_night_light_initialized = false; - -static gsr_cursor x11_cursor; -static Display *x11_cursor_display = NULL; - -static gsr_capture* create_monitor_capture(const args_parser &arg_parser, gsr_egl *egl, const CaptureSource &capture_source, bool prefer_ximage) { - if(gsr_window_get_display_server(egl->window) == GSR_DISPLAY_SERVER_X11 && prefer_ximage) { - gsr_capture_ximage_params ximage_params; - memset(&ximage_params, 0, sizeof(ximage_params)); - ximage_params.egl = egl; - ximage_params.cursor = &x11_cursor; - ximage_params.display_to_capture = capture_source.name.c_str(); - ximage_params.record_cursor = arg_parser.settings.record_cursor; - ximage_params.output_resolution = arg_parser.settings.output_resolution; - ximage_params.region_size = capture_source.region_size; - ximage_params.region_position = capture_source.region_pos; - return gsr_capture_ximage_create(&ximage_params); - } - - if(monitor_capture_use_drm(egl->window, egl->gpu_info.vendor)) { - if(!kms_client_initialized) { - kms_client_initialized = true; - const int kms_init_res = gsr_kms_client_init(&kms_client, egl->card_path); - if(kms_init_res != 0) - _exit(kms_init_res < 0 ? 1 : kms_init_res); - } - - if(!kde_night_light_initialized && gsr_window_get_display_server(egl->window) == GSR_DISPLAY_SERVER_WAYLAND) { - kde_night_light_initialized = true; - kde_night_light = gsr_kde_night_light_create(); - } - - gsr_capture_kms_params kms_params; - memset(&kms_params, 0, sizeof(kms_params)); - kms_params.egl = egl; - kms_params.x11_cursor = &x11_cursor; - kms_params.kms_response = &kms_response; - kms_params.kde_night_light = kde_night_light; - kms_params.display_to_capture = capture_source.name.c_str(); - kms_params.record_cursor = arg_parser.settings.record_cursor; - kms_params.hdr = video_codec_is_hdr(arg_parser.settings.video_codec); - kms_params.fps = arg_parser.settings.fps; - kms_params.output_resolution = arg_parser.settings.output_resolution; - kms_params.region_size = capture_source.region_size; - kms_params.region_position = capture_source.region_pos; - return gsr_capture_kms_create(&kms_params); - } else { - const char *capture_source_real = capture_source.name.c_str(); - const bool direct_capture = strcmp(capture_source.name.c_str(), "screen-direct") == 0 || strcmp(capture_source.name.c_str(), "screen-direct-force") == 0; - if(direct_capture) { - capture_source_real = "screen"; - gsr_log(GSR_LOG_LEVEL_WARNING, "%s capture option is not recommended unless you use G-SYNC as Nvidia has driver issues that can cause your system or games to freeze/crash.", capture_source.name.c_str()); - } - - gsr_capture_nvfbc_params nvfbc_params; - memset(&nvfbc_params, 0, sizeof(nvfbc_params)); - nvfbc_params.egl = egl; - nvfbc_params.display_to_capture = capture_source_real; - nvfbc_params.fps = arg_parser.settings.fps; - nvfbc_params.direct_capture = direct_capture; - nvfbc_params.record_cursor = arg_parser.settings.record_cursor; - nvfbc_params.output_resolution = arg_parser.settings.output_resolution; - nvfbc_params.region_size = capture_source.region_size; - nvfbc_params.region_position = capture_source.region_pos; - return gsr_capture_nvfbc_create(&nvfbc_params); - } -} - -static void monitor_output_callback_print_region(const gsr_monitor *monitor, void *userdata) { - const vec2i monitor_position = monitor->logical_pos; - const vec2i monitor_size = monitor->logical_size; - fprintf(stderr, " \"%.*s\" (%dx%d+%d+%d)\n", monitor->name_len, monitor->name, monitor_size.x, monitor_size.y, monitor_position.x, monitor_position.y); -} - -static std::string region_get_data(gsr_egl *egl, vec2i *region_size, vec2i *region_position) { - vec2i monitor_pos = {0, 0}; - vec2i monitor_size = {0, 0}; - double monitor_scale_inverted = 1.0; - std::string window = get_monitor_by_region_center(egl, *region_position, *region_size, &monitor_pos, &monitor_size, &monitor_scale_inverted); - if(window.empty()) { - const bool is_x11 = gsr_window_get_display_server(egl->window) == GSR_DISPLAY_SERVER_X11; - const gsr_connection_type connection_type = is_x11 ? GSR_CONNECTION_X11 : GSR_CONNECTION_WAYLAND; - gsr_log(GSR_LOG_LEVEL_ERROR, "the region %dx%d+%d+%d doesn't match any monitor. Available monitors and their regions:", region_size->x, region_size->y, region_position->x, region_position->y); - - MonitorOutputCallbackUserdata userdata; - userdata.window = egl->window; - for_each_active_monitor_output(egl->window, egl->card_path, connection_type, monitor_output_callback_print_region, &userdata); - _exit(51); - } - - // Capture whole monitor when region size is set to 0x0 - if(region_size->x == 0 && region_size->y == 0) { - region_position->x = 0; - region_position->y = 0; - } else { - region_position->x -= monitor_pos.x; - region_position->y -= monitor_pos.y; - // Match drm plane coordinate space (1x scaling) to wayland coordinate space (which may have scaling set by user) - region_position->x *= monitor_scale_inverted; - region_position->y *= monitor_scale_inverted; - - region_size->x *= monitor_scale_inverted; - region_size->y *= monitor_scale_inverted; - } - return window; -} - -static gsr_capture* create_capture_impl(const args_parser &arg_parser, gsr_egl *egl, CaptureSource &capture_source, bool prefer_ximage) { - bool follow_focused = false; - const bool wayland = gsr_window_get_display_server(egl->window) == GSR_DISPLAY_SERVER_WAYLAND; - - gsr_capture *capture = nullptr; - if(capture_source.type == GSR_CAPTURE_SOURCE_TYPE_FOCUSED_WINDOW) { - if(wayland) { - gsr_log(GSR_LOG_LEVEL_ERROR, "GPU Screen Recorder window capture only works in a pure X11 session. Xwayland is not supported. You can record a monitor instead on wayland"); - _exit(2); - } - - if(arg_parser.settings.output_resolution.x <= 0 || arg_parser.settings.output_resolution.y <= 0) { - gsr_log(GSR_LOG_LEVEL_ERROR, "invalid value for option -s '%dx%d' when using -w focused option. expected width and height to be greater than 0", arg_parser.settings.output_resolution.x, arg_parser.settings.output_resolution.y); - args_parser_print_usage(); - _exit(1); - } - - follow_focused = true; - } else if(capture_source.type == GSR_CAPTURE_SOURCE_TYPE_PORTAL) { -#ifdef GSR_PORTAL - // Desktop portal capture on x11 doesn't seem to be hardware accelerated - if(!wayland) { - gsr_log(GSR_LOG_LEVEL_ERROR, "desktop portal capture is not supported on X11"); - _exit(1); - } - - gsr_capture_portal_params portal_params; - memset(&portal_params, 0, sizeof(portal_params)); - portal_params.egl = egl; - portal_params.record_cursor = arg_parser.settings.record_cursor; - portal_params.restore_portal_session = arg_parser.settings.restore_portal_session; - portal_params.portal_session_token_filepath = arg_parser.settings.portal_session_token_filepath; - portal_params.output_resolution = arg_parser.settings.output_resolution; - capture = gsr_capture_portal_create(&portal_params); - if(!capture) - _exit(1); -#else - gsr_log(GSR_LOG_LEVEL_ERROR, "option '-w portal' used but GPU Screen Recorder was compiled without desktop portal support. Please recompile GPU Screen recorder with the -Dportal=true option"); - _exit(2); -#endif - } else if(capture_source.type == GSR_CAPTURE_SOURCE_TYPE_REGION) { - capture_source.name = region_get_data(egl, &capture_source.region_size, &capture_source.region_pos); - capture = create_monitor_capture(arg_parser, egl, capture_source, prefer_ximage); - if(!capture) - _exit(1); - } else if(capture_source.type == GSR_CAPTURE_SOURCE_TYPE_MONITOR) { - capture_source.name = validate_monitor_get_valid(egl, capture_source.name.c_str()); - capture = create_monitor_capture(arg_parser, egl, capture_source, prefer_ximage); - if(!capture) - _exit(1); - } else if(capture_source.type == GSR_CAPTURE_SOURCE_TYPE_V4L2) { - gsr_capture_v4l2_params v4l2_params; - memset(&v4l2_params, 0, sizeof(v4l2_params)); - v4l2_params.egl = egl; - v4l2_params.output_resolution = arg_parser.settings.output_resolution; - v4l2_params.device_path = capture_source.name.c_str(); - v4l2_params.pixfmt = capture_source.v4l2_pixfmt; - v4l2_params.camera_fps = capture_source.camera_fps; - v4l2_params.camera_resolution.width = capture_source.camera_resolution.x; - v4l2_params.camera_resolution.height = capture_source.camera_resolution.y; - capture = gsr_capture_v4l2_create(&v4l2_params); - if(!capture) - _exit(1); - } else { - if(wayland) { - gsr_log(GSR_LOG_LEVEL_ERROR, "GPU Screen Recorder window capture only works in a pure X11 session. Xwayland is not supported. You can record a monitor instead on wayland or use -w portal option which supports window capture if your wayland compositor supports window capture"); - _exit(2); - } - } - - if(!capture) { - gsr_capture_xcomposite_params xcomposite_params; - memset(&xcomposite_params, 0, sizeof(xcomposite_params)); - xcomposite_params.egl = egl; - xcomposite_params.cursor = &x11_cursor; - xcomposite_params.window = capture_source.window_id; - xcomposite_params.follow_focused = follow_focused; - xcomposite_params.record_cursor = arg_parser.settings.record_cursor; - xcomposite_params.output_resolution = arg_parser.settings.output_resolution; - capture = gsr_capture_xcomposite_create(&xcomposite_params); - if(!capture) - _exit(1); - } - - return capture; -} +static gsr_capture_deps capture_deps; static gsr_color_range image_format_to_color_range(gsr_image_format image_format, int image_quality) { switch(image_format) { @@ -1384,115 +1045,6 @@ static int video_quality_to_image_quality_value(gsr_video_quality video_quality) return 90; } -static bool any_video_sources_uses_external_image(std::vector &video_sources) { - for(VideoSource &video_source : video_sources) { - if(gsr_capture_uses_external_image(video_source.capture)) - return true; - } - return false; -} - -static std::vector create_video_sources(const args_parser &arg_parser, gsr_egl *egl, bool prefer_ximage, std::vector &capture_sources, vec2i &video_size) { - std::vector video_sources; - video_sources.reserve(capture_sources.size()); - - for(CaptureSource &capture_source : capture_sources) { - gsr_capture_metadata capture_metadata; - memset(&capture_metadata, 0, sizeof(capture_metadata)); - capture_metadata.fps = arg_parser.settings.fps; - capture_metadata.halign = capture_source.halign; - capture_metadata.valign = capture_source.valign; - capture_metadata.flip = (gsr_flip)capture_source.flip; - video_sources.push_back(VideoSource{create_capture_impl(arg_parser, egl, capture_source, prefer_ximage), capture_metadata, &capture_source}); - } - - for(VideoSource &video_source : video_sources) { - int capture_result = gsr_capture_start(video_source.capture, &video_source.metadata); - if(capture_result != 0) { - gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_capture_start failed"); - _exit(capture_result); - } - } - - vec2i start_pos = {99999, 99999}; - vec2i end_pos = {-99999, -99999}; - for(const VideoSource &video_source : video_sources) { - // TODO: Skip scalar positions for now, but this should be handled in a better way. - // Maybe handle scalars at the next loop by multiplying video size by the scalar. - if(video_source.capture_source->pos.x_type == VVEC2I_TYPE_SCALAR || video_source.capture_source->pos.y_type == VVEC2I_TYPE_SCALAR - || (video_source.capture_source->size.x_type == VVEC2I_TYPE_SCALAR && video_source.capture_source->size.x != 100) - || (video_source.capture_source->size.y_type == VVEC2I_TYPE_SCALAR && video_source.capture_source->size.y != 100)) - { - continue; - } - const vec2i video_source_start_pos = {video_source.capture_source->pos.x, video_source.capture_source->pos.y}; - const vec2i video_source_end_pos = {video_source_start_pos.x + video_source.metadata.video_size.x, video_source_start_pos.y + video_source.metadata.video_size.y}; - - start_pos.x = std::min(start_pos.x, video_source_start_pos.x); - start_pos.y = std::min(start_pos.y, video_source_start_pos.y); - - end_pos.x = std::max(end_pos.x, video_source_end_pos.x); - end_pos.y = std::max(end_pos.y, video_source_end_pos.y); - } - - video_size.x = std::max(0, end_pos.x - start_pos.x); - video_size.y = std::max(0, end_pos.y - start_pos.y); - - for(VideoSource &video_source : video_sources) { - video_source.metadata.video_size = video_size; - } - - return video_sources; -} - -static void video_sources_update_with_real_video_size(std::vector &capture_sources, std::vector &video_sources, vec2i video_size) { - assert(capture_sources.size() == video_sources.size()); - for(size_t i = 0; i < capture_sources.size(); ++i) { - CaptureSource &capture_source = capture_sources[i]; - VideoSource &video_source = video_sources[i]; - - video_source.metadata.recording_size = video_source.metadata.video_size; - // TODO: What if this updated resolution is above max resolution? - video_source.metadata.video_size = video_size; - - if(capture_source.pos.x != 0 || capture_source.pos.y != 0) { - video_source.metadata.position.x = capture_source.pos.x; - video_source.metadata.position.y = capture_source.pos.y; - - if(capture_source.pos.x_type == VVEC2I_TYPE_SCALAR) - video_source.metadata.position.x = video_source.metadata.video_size.x * ((double)video_source.metadata.position.x / 100.0); - - if(capture_source.pos.y_type == VVEC2I_TYPE_SCALAR) - video_source.metadata.position.y = video_source.metadata.video_size.y * ((double)video_source.metadata.position.y / 100.0); - } - - if(capture_source.size.x != 0 || capture_source.size.y != 0) { - video_source.metadata.recording_size.x = capture_source.size.x; - video_source.metadata.recording_size.y = capture_source.size.y; - - if(capture_source.size.x_type == VVEC2I_TYPE_SCALAR) - video_source.metadata.recording_size.x = video_source.metadata.video_size.x * ((double)video_source.metadata.recording_size.x / 100.0); - - if(capture_source.size.y_type == VVEC2I_TYPE_SCALAR) - video_source.metadata.recording_size.y = video_source.metadata.video_size.y * ((double)video_source.metadata.recording_size.y / 100.0); - } - } -} - -static void gsr_capture_kms_cleanup_kms_fds() { - for(int i = 0; i < kms_response.num_items; ++i) { - for(int j = 0; j < kms_response.items[i].num_dma_bufs; ++j) { - gsr_kms_response_dma_buf *dma_buf = &kms_response.items[i].dma_buf[j]; - if(dma_buf->fd > 0) { - close(dma_buf->fd); - dma_buf->fd = -1; - } - } - kms_response.items[i].num_dma_bufs = 0; - } - kms_response.num_items = 0; -} - static void load_plugins(gsr_plugins *plugins, args_parser &arg_parser, gsr_egl *egl, vec2i video_size) { const Arg *plugin_arg = args_parser_get_arg(&arg_parser, "-p"); assert(plugin_arg); @@ -1520,14 +1072,18 @@ static void load_plugins(gsr_plugins *plugins, args_parser &arg_parser, gsr_egl } // TODO: 10-bit and hdr. -static void capture_image_to_file(args_parser &arg_parser, gsr_egl *egl, gsr_window *window, gsr_image_format image_format, std::vector &capture_sources) { +static void capture_image_to_file(args_parser &arg_parser, gsr_egl *egl, gsr_window *window, gsr_image_format image_format, gsr_capture_sources *capture_sources) { const int image_quality = video_quality_to_image_quality_value(arg_parser.settings.video_quality); const gsr_color_range color_range = image_format_to_color_range(image_format, image_quality); arg_parser.settings.fps = 60; // We want to capture an image as soon as possible vec2i video_size = {0, 0}; - std::vector video_sources = create_video_sources(arg_parser, egl, true, capture_sources, video_size); - video_sources_update_with_real_video_size(capture_sources, video_sources, video_size); + gsr_video_sources video_sources_data; + const int video_sources_result = gsr_video_sources_create(&video_sources_data, &arg_parser.settings, egl, &capture_deps, true, capture_sources, &video_size); + if(video_sources_result != GSR_ERROR_OK) + _exit(gsr_error_to_exit_code(video_sources_result)); + gsr_video_sources *video_sources = &video_sources_data; + gsr_video_sources_update_with_real_video_size(video_sources, video_size); const Arg *plugin_arg = args_parser_get_arg(&arg_parser, "-p"); assert(plugin_arg); @@ -1548,7 +1104,7 @@ static void capture_image_to_file(args_parser &arg_parser, gsr_egl *egl, gsr_win memset(&color_conversion_params, 0, sizeof(color_conversion_params)); color_conversion_params.color_range = color_range; color_conversion_params.egl = egl; - color_conversion_params.load_external_image_shader = any_video_sources_uses_external_image(video_sources); + color_conversion_params.load_external_image_shader = gsr_video_sources_uses_external_image(video_sources); color_conversion_params.destination_textures[0] = image_writer.texture; color_conversion_params.destination_textures_size[0] = video_size; @@ -1570,26 +1126,25 @@ static void capture_image_to_file(args_parser &arg_parser, gsr_egl *egl, gsr_win while(running) { while(gsr_window_process_event(window)) { - if(x11_cursor_display && arg_parser.settings.record_cursor) - gsr_cursor_on_event(&x11_cursor, gsr_window_get_event_data(window)); + if(capture_deps.x11_cursor_display && arg_parser.settings.record_cursor) + gsr_cursor_on_event(&capture_deps.x11_cursor, gsr_window_get_event_data(window)); - for(VideoSource &video_source : video_sources) { + for(size_t video_source_index = 0; video_source_index < video_sources->num_items; ++video_source_index) { + gsr_video_source &video_source = video_sources->items[video_source_index]; gsr_capture_on_event(video_source.capture, egl); } } - if(x11_cursor_display && arg_parser.settings.record_cursor) - gsr_cursor_tick(&x11_cursor, DefaultRootWindow(x11_cursor_display)); + if(capture_deps.x11_cursor_display && arg_parser.settings.record_cursor) + gsr_cursor_tick(&capture_deps.x11_cursor, DefaultRootWindow(capture_deps.x11_cursor_display)); - gsr_capture_kms_cleanup_kms_fds(); + gsr_capture_deps_cleanup_kms_fds(&capture_deps); - if(kms_client_initialized) { - if(gsr_kms_client_get_kms(&kms_client, &kms_response) != 0) - gsr_log(GSR_LOG_LEVEL_ERROR, "failed to get kms, error: %d (%s)", kms_response.result, kms_response.err_msg); - } + gsr_capture_deps_update_kms(&capture_deps); should_stop_error = false; - for(VideoSource &video_source : video_sources) { + for(size_t video_source_index = 0; video_source_index < video_sources->num_items; ++video_source_index) { + gsr_video_source &video_source = video_sources->items[video_source_index]; gsr_capture_tick(video_source.capture); if(gsr_capture_should_stop(video_source.capture, &should_stop_error)) { running = 0; @@ -1597,7 +1152,8 @@ static void capture_image_to_file(args_parser &arg_parser, gsr_egl *egl, gsr_win } } - for(VideoSource &video_source : video_sources) { + for(size_t video_source_index = 0; video_source_index < video_sources->num_items; ++video_source_index) { + gsr_video_source &video_source = video_sources->items[video_source_index]; if(video_source.capture->pre_capture) video_source.capture->pre_capture(video_source.capture, &video_source.metadata, output_color_conversion); } @@ -1608,14 +1164,15 @@ static void capture_image_to_file(args_parser &arg_parser, gsr_egl *egl, gsr_win } bool all_sources_captured = true; - for(VideoSource &video_source : video_sources) { + for(size_t video_source_index = 0; video_source_index < video_sources->num_items; ++video_source_index) { + gsr_video_source &video_source = video_sources->items[video_source_index]; // It can fail, for example when capturing portal and the target is a monitor that hasn't been updated. // This can also happen for example if the system suspends and the monitor to capture's framebuffer is gone, or if the target window disappeared. if(gsr_capture_capture(video_source.capture, &video_source.metadata, output_color_conversion) != 0) all_sources_captured = false; } - gsr_capture_kms_cleanup_kms_fds(); + gsr_capture_deps_cleanup_kms_fds(&capture_deps); if(all_sources_captured) break; @@ -1646,9 +1203,7 @@ static void capture_image_to_file(args_parser &arg_parser, gsr_egl *egl, gsr_win gsr_plugins_deinit(&plugins); gsr_image_writer_deinit(&image_writer); - for(VideoSource &video_source : video_sources) { - gsr_capture_destroy(video_source.capture); - } + gsr_video_sources_deinit(video_sources); _exit(should_stop_error ? 3 : 0); } @@ -1782,293 +1337,6 @@ static std::vector parse_audio_inputs(const gsr_audio_devices return requested_audio_inputs; } -static bool is_hex_num(char c) { - return (c >= 'A' && c <= 'F') || (c >= 'a' && c <= 'f') || (c >= '0' && c <= '9'); -} - -static bool contains_non_hex_number(const char *str) { - bool hex_start = false; - size_t len = strlen(str); - if(len >= 2 && memcmp(str, "0x", 2) == 0) { - str += 2; - len -= 2; - hex_start = true; - } - - bool is_hex = false; - for(size_t i = 0; i < len; ++i) { - char c = str[i]; - if(c == '\0') - return false; - if(!is_hex_num(c)) - return true; - if((c >= 'A' && c <= 'F') || (c >= 'a' && c <= 'f')) - is_hex = true; - } - - return is_hex && !hex_start; -} - -static void capture_source_type_from_string(const char *capture_source_str, size_t size, CaptureSource &capture_source) { - char capture_source_str_n[64]; - snprintf(capture_source_str_n, sizeof(capture_source_str_n), "%.*s", (int)size, capture_source_str); - - if(size == 7 && memcmp(capture_source_str_n, "focused", 7) == 0) { - capture_source.type = GSR_CAPTURE_SOURCE_TYPE_FOCUSED_WINDOW; - } else if(size == 6 && memcmp(capture_source_str_n, "portal", 6) == 0) { - capture_source.type = GSR_CAPTURE_SOURCE_TYPE_PORTAL; - } else if(size == 6 && memcmp(capture_source_str_n, "region", 6) == 0) { - capture_source.type = GSR_CAPTURE_SOURCE_TYPE_REGION; - } else if(size >= 10 && memcmp(capture_source_str_n, "/dev/video", 10) == 0) { - capture_source.type = GSR_CAPTURE_SOURCE_TYPE_V4L2; - } else if(sscanf(capture_source_str_n, "%dx%d+%d+%d", &capture_source.region_size.x, &capture_source.region_size.y, &capture_source.region_pos.x, &capture_source.region_pos.y) == 4) { - capture_source.type = GSR_CAPTURE_SOURCE_TYPE_REGION; - capture_source.region_set = true; - } else if(contains_non_hex_number(capture_source_str_n)) { - capture_source.type = GSR_CAPTURE_SOURCE_TYPE_MONITOR; - } else { - capture_source.type = GSR_CAPTURE_SOURCE_TYPE_WINDOW; - } -} - -static bool string_to_capture_alignment(const char *str, size_t len, gsr_capture_alignment *alignment) { - if(len == 5 && memcmp(str, "start", 5) == 0) { - *alignment = GSR_CAPTURE_ALIGN_START; - return true; - } else if(len == 6 && memcmp(str, "center", 6) == 0) { - *alignment = GSR_CAPTURE_ALIGN_CENTER; - return true; - } else if(len == 3 && memcmp(str, "end", 3) == 0) { - *alignment = GSR_CAPTURE_ALIGN_END; - return true; - } else { - return false; - } -} - -static bool string_to_v4l2_pixfmt(const char *str, size_t len, gsr_capture_v4l2_pixfmt *pixfmt) { - if(len == 4 && memcmp(str, "auto", 4) == 0) { - *pixfmt = GSR_CAPTURE_V4L2_PIXFMT_AUTO; - return true; - } else if(len == 4 && memcmp(str, "yuyv", 4) == 0) { - *pixfmt = GSR_CAPTURE_V4L2_PIXFMT_YUYV; - return true; - } else if(len == 5 && memcmp(str, "mjpeg", 5) == 0) { - *pixfmt = GSR_CAPTURE_V4L2_PIXFMT_MJPEG; - return true; - } else { - return false; - } -} - -static bool string_to_bool(const char *str, size_t len, bool *value) { - if(len == 4 && memcmp(str, "true", 4) == 0) { - *value = true; - return true; - } else if(len == 5 && memcmp(str, "false", 5) == 0) { - *value = false; - return true; - } else { - return false; - } -} - -struct ParseCaptureSourceOptionsUserdata { - CaptureSource *capture_source; - bool is_first_column; -}; - -static bool parse_capture_source_options_callback(const char *sub, size_t size, void *userdata) { - ParseCaptureSourceOptionsUserdata *parse_userdata = (ParseCaptureSourceOptionsUserdata*)userdata; - CaptureSource &capture_source = *parse_userdata->capture_source; - if(size == 0) - return true; - - if(parse_userdata->is_first_column) { - parse_userdata->is_first_column = false; - return true; - } - - if(gsr_string_starts_with(sub, size, "x=")) { - capture_source.pos.x_type = sub[size - 1] == '%' ? VVEC2I_TYPE_SCALAR : VVEC2I_TYPE_PIXELS; - sub += 2; - size -= 2; - if(!gsr_string_to_int(sub, size, &capture_source.pos.x)) { - gsr_log(GSR_LOG_LEVEL_ERROR, "invalid capture target value for option x: \"%.*s\", expected a number", (int)size, sub); - _exit(1); - } - } else if(gsr_string_starts_with(sub, size, "y=")) { - capture_source.pos.y_type = sub[size - 1] == '%' ? VVEC2I_TYPE_SCALAR : VVEC2I_TYPE_PIXELS; - sub += 2; - size -= 2; - if(!gsr_string_to_int(sub, size, &capture_source.pos.y)) { - gsr_log(GSR_LOG_LEVEL_ERROR, "invalid capture target value for option y: \"%.*s\", expected a number", (int)size, sub); - _exit(1); - } - - } else if(gsr_string_starts_with(sub, size, "width=")) { - capture_source.size.x_type = sub[size - 1] == '%' ? VVEC2I_TYPE_SCALAR : VVEC2I_TYPE_PIXELS; - sub += 6; - size -= 6; - if(!gsr_string_to_int(sub, size, &capture_source.size.x)) { - gsr_log(GSR_LOG_LEVEL_ERROR, "invalid capture target value for option width: \"%.*s\", expected a number", (int)size, sub); - _exit(1); - } - } else if(gsr_string_starts_with(sub, size, "height=")) { - capture_source.size.y_type = sub[size - 1] == '%' ? VVEC2I_TYPE_SCALAR : VVEC2I_TYPE_PIXELS; - sub += 7; - size -= 7; - if(!gsr_string_to_int(sub, size, &capture_source.size.y)) { - gsr_log(GSR_LOG_LEVEL_ERROR, "invalid capture target value for option height: \"%.*s\", expected a number", (int)size, sub); - _exit(1); - } - } else if(gsr_string_starts_with(sub, size, "halign=")) { - sub += 7; - size -= 7; - if(!string_to_capture_alignment(sub, size, &capture_source.halign)) { - gsr_log(GSR_LOG_LEVEL_ERROR, "invalid capture target value for option halign: \"%.*s\", expected a \"start\", \"center\" or \"end\"", (int)size, sub); - _exit(1); - } - } else if(gsr_string_starts_with(sub, size, "valign=")) { - sub += 7; - size -= 7; - if(!string_to_capture_alignment(sub, size, &capture_source.valign)) { - gsr_log(GSR_LOG_LEVEL_ERROR, "invalid capture target value for option valign: \"%.*s\", expected a \"start\", \"center\" or \"end\"", (int)size, sub); - _exit(1); - } - } else if(gsr_string_starts_with(sub, size, "pixfmt=")) { - sub += 7; - size -= 7; - if(!string_to_v4l2_pixfmt(sub, size, &capture_source.v4l2_pixfmt)) { - gsr_log(GSR_LOG_LEVEL_ERROR, "invalid v4l2 pixfmt value for option pixfmt: \"%.*s\", expected a \"auto\", \"yuyv\" or \"mjpeg\"", (int)size, sub); - _exit(1); - } - } else if(gsr_string_starts_with(sub, size, "hflip=")) { - sub += 6; - size -= 6; - bool hflip = false; - if(!string_to_bool(sub, size, &hflip)) { - gsr_log(GSR_LOG_LEVEL_ERROR, "invalid bool value for option hflip: \"%.*s\", expected a \"true\" or \"false\"", (int)size, sub); - _exit(1); - } - - if(hflip) - capture_source.flip |= GSR_FLIP_HORIZONTAL; - } else if(gsr_string_starts_with(sub, size, "vflip=")) { - sub += 6; - size -= 6; - bool vflip = false; - if(!string_to_bool(sub, size, &vflip)) { - gsr_log(GSR_LOG_LEVEL_ERROR, "invalid bool value for option vflip: \"%.*s\", expected a \"true\" or \"false\"", (int)size, sub); - _exit(1); - } - - if(vflip) - capture_source.flip |= GSR_FLIP_VERTICAL; - } else if(gsr_string_starts_with(sub, size, "camera_fps=")) { - sub += 11; - size -= 11; - if(!gsr_string_to_int(sub, size, &capture_source.camera_fps)) { - gsr_log(GSR_LOG_LEVEL_ERROR, "invalid capture target value for option camera_fps: \"%.*s\", expected a number", (int)size, sub); - _exit(1); - } - } else if(gsr_string_starts_with(sub, size, "camera_width=")) { - sub += 13; - size -= 13; - if(!gsr_string_to_int(sub, size, &capture_source.camera_resolution.x)) { - gsr_log(GSR_LOG_LEVEL_ERROR, "invalid capture target value for option camera_width: \"%.*s\", expected a number", (int)size, sub); - _exit(1); - } - } else if(gsr_string_starts_with(sub, size, "camera_height=")) { - sub += 14; - size -= 14; - if(!gsr_string_to_int(sub, size, &capture_source.camera_resolution.y)) { - gsr_log(GSR_LOG_LEVEL_ERROR, "invalid capture target value for option camera_height: \"%.*s\", expected a number", (int)size, sub); - _exit(1); - } - } else { - gsr_log(GSR_LOG_LEVEL_ERROR, "invalid capture target option \"%.*s\", expected x, y, width, height, halign, valign, pixfmt, hflip, vflip, camera_fps, camera_width or camera_height", (int)size, sub); - _exit(1); - } - - return true; -} - -static void parse_capture_source_options(const std::string &capture_source_str, CaptureSource &capture_source) { - ParseCaptureSourceOptionsUserdata userdata; - userdata.capture_source = &capture_source; - userdata.is_first_column = true; - gsr_string_split(capture_source_str.c_str(), ';', parse_capture_source_options_callback, &userdata); -} - -struct ParseCaptureSourceArgUserdata { - std::vector *requested_capture_sources; - const args_parser *arg_parser; - bool has_multiple_capture_sources; -}; - -static bool parse_capture_source_arg_callback(const char *sub, size_t size, void *userdata) { - ParseCaptureSourceArgUserdata *parse_userdata = (ParseCaptureSourceArgUserdata*)userdata; - if(size == 0) - return true; - - const char *substr_start = sub; - size_t capture_source_size = size; - const char *capture_source_end = (const char*)memchr(sub, ';', size); - if(capture_source_end) - capture_source_size = capture_source_end - sub; - - CaptureSource capture_source; - capture_source.region_pos = parse_userdata->arg_parser->settings.region_position; - capture_source.region_size = parse_userdata->arg_parser->settings.region_size; - - if(gsr_string_starts_with(sub, capture_source_size, "monitor:")) { - capture_source.type = GSR_CAPTURE_SOURCE_TYPE_MONITOR; - sub += 8; - capture_source_size -= 8; - } else if(gsr_string_starts_with(sub, capture_source_size, "window:")) { - capture_source.type = GSR_CAPTURE_SOURCE_TYPE_WINDOW; - sub += 7; - capture_source_size -= 7; - } else if(gsr_string_starts_with(sub, capture_source_size, "v4l2:")) { - capture_source.type = GSR_CAPTURE_SOURCE_TYPE_V4L2; - sub += 5; - capture_source_size -= 5; - } else { - capture_source_type_from_string(sub, capture_source_size, capture_source); - } - - capture_source.name.assign(sub, capture_source_size); - - if(capture_source.type == GSR_CAPTURE_SOURCE_TYPE_WINDOW) { - if(!gsr_string_to_int64(capture_source.name.c_str(), capture_source.name.size(), &capture_source.window_id)) { - gsr_log(GSR_LOG_LEVEL_ERROR, "invalid window number %s", capture_source.name.c_str()); - args_parser_print_usage(); - _exit(1); - } - } - - if(parse_userdata->has_multiple_capture_sources) { - capture_source.halign = GSR_CAPTURE_ALIGN_START; - capture_source.valign = GSR_CAPTURE_ALIGN_START; - capture_source.pos = {0, 0, VVEC2I_TYPE_PIXELS, VVEC2I_TYPE_PIXELS}; - } - - parse_capture_source_options(std::string(substr_start, size), capture_source); - parse_userdata->requested_capture_sources->push_back(capture_source); - return true; -} - -static std::vector parse_capture_source_arg(const char *capture_source_arg, const args_parser &arg_parser) { - std::vector requested_capture_sources; - ParseCaptureSourceArgUserdata userdata; - userdata.requested_capture_sources = &requested_capture_sources; - userdata.arg_parser = &arg_parser; - userdata.has_multiple_capture_sources = strchr(capture_source_arg, '|') != nullptr; - gsr_string_split(capture_source_arg, '|', parse_capture_source_arg_callback, &userdata); - return requested_capture_sources; -} - static bool audio_inputs_has_app_audio(const std::vector &audio_inputs) { for(const auto &audio_input : audio_inputs) { if(audio_input.type == AudioInputType::APPLICATION) @@ -2278,54 +1546,22 @@ static void set_display_server_environment_variables() { } } -static bool is_capturing_damage_tracked_target(const std::vector &capture_sources) { - for(const CaptureSource &capture_source : capture_sources) { - if(capture_source.type != GSR_CAPTURE_SOURCE_TYPE_V4L2) - return true; - } - return false; -} - -static bool is_capturing_type(const std::vector &capture_sources, CaptureSourceType target_type) { - for(const CaptureSource &capture_source : capture_sources) { - if(capture_source.type == target_type) - return true; - } - return false; -} - -static bool has_capture_source_with_region_set(const std::vector &capture_sources) { - for(const CaptureSource &capture_source : capture_sources) { - if(capture_source.type == GSR_CAPTURE_SOURCE_TYPE_REGION && capture_source.region_set) - return true; - } - return false; -} - -static bool is_capturing_monitor_or_region(const std::vector &capture_sources) { - for(const CaptureSource &capture_source : capture_sources) { - if(capture_source.type == GSR_CAPTURE_SOURCE_TYPE_MONITOR || capture_source.type == GSR_CAPTURE_SOURCE_TYPE_REGION) - return true; - } - return false; -} - -static void validate_args_with_capture_sources(args_parser &arg_parser, const std::vector &capture_sources) { +static void 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(is_capturing_type(capture_sources, GSR_CAPTURE_SOURCE_TYPE_FOCUSED_WINDOW) && output_resolution_arg->num_values == 0) { + 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(); _exit(1); } - const bool is_capturing_region = is_capturing_type(capture_sources, GSR_CAPTURE_SOURCE_TYPE_REGION); + 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 && !has_capture_source_with_region_set(capture_sources)) { + 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(); _exit(1); @@ -2340,7 +1576,7 @@ static void validate_args_with_capture_sources(args_parser &arg_parser, const st } } - if(!arg_parser.settings.restore_portal_session && is_capturing_type(capture_sources, GSR_CAPTURE_SOURCE_TYPE_PORTAL)) + 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"); } @@ -2453,8 +1689,13 @@ int main(int argc, char **argv) { setenv("AMD_DEBUG", "lowlatencyenc", true); } - std::vector capture_sources = parse_capture_source_arg(arg_parser.settings.capture_source, arg_parser); - if(capture_sources.empty()) { + gsr_capture_sources capture_sources_data; + const int parse_capture_sources_result = gsr_capture_sources_parse(&capture_sources_data, arg_parser.settings.capture_source, arg_parser.settings.region_position, arg_parser.settings.region_size); + if(parse_capture_sources_result != GSR_ERROR_OK) + _exit(gsr_error_to_exit_code(parse_capture_sources_result)); + + gsr_capture_sources *capture_sources = &capture_sources_data; + 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(1); @@ -2507,7 +1748,7 @@ int main(int argc, char **argv) { gsr_windowing windowing; gsr_windowing_params windowing_params; - windowing_params.monitor_capture = is_capturing_monitor_or_region(capture_sources); + 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) @@ -2516,7 +1757,7 @@ int main(int argc, char **argv) { Display *dpy = windowing.display; gsr_window *window = windowing.window; - if(is_capturing_type(capture_sources, GSR_CAPTURE_SOURCE_TYPE_PORTAL)) { + 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(); @@ -2546,12 +1787,7 @@ int main(int argc, char **argv) { _exit(2); } - memset(&x11_cursor, 0, sizeof(x11_cursor)); - x11_cursor_display = NULL; - if(gsr_window_get_display_server(window) == GSR_DISPLAY_SERVER_X11 && arg_parser.settings.record_cursor) { - x11_cursor_display = (Display*)gsr_window_get_display(egl.window); - gsr_cursor_init(&x11_cursor, &egl, x11_cursor_display); - } + gsr_capture_deps_init_cursor(&capture_deps, &egl, arg_parser.settings.record_cursor); // if(wayland && arg_parser.capture_source_type == GSR_CAPTURE_SOURCE_TYPE_MONITOR) { // fprintf(stderr, "gsr warning: it's not possible to sync video to recorded monitor exactly on wayland when recording a monitor." @@ -2604,7 +1840,12 @@ int main(int argc, char **argv) { arg_parser.settings.audio_codec = select_audio_codec_with_fallback(arg_parser.settings.audio_codec, file_extension.c_str(), uses_amix); vec2i video_size = {0, 0}; - std::vector video_sources = create_video_sources(arg_parser, &egl, false, capture_sources, video_size); + gsr_video_sources video_sources_data; + const int video_sources_result = gsr_video_sources_create(&video_sources_data, &arg_parser.settings, &egl, &capture_deps, false, capture_sources, &video_size); + if(video_sources_result != GSR_ERROR_OK) + _exit(gsr_error_to_exit_code(video_sources_result)); + + gsr_video_sources *video_sources = &video_sources_data; // (Some?) livestreaming services require at least one audio track to work. // If not audio is provided then create one silent audio track. @@ -2668,7 +1909,7 @@ int main(int argc, char **argv) { video_size.x = video_codec_context->width; video_size.y = video_codec_context->height; - video_sources_update_with_real_video_size(capture_sources, video_sources, video_size); + gsr_video_sources_update_with_real_video_size(video_sources, video_size); const Arg *plugin_arg = args_parser_get_arg(&arg_parser, "-p"); assert(plugin_arg); @@ -2683,7 +1924,7 @@ int main(int argc, char **argv) { memset(&color_conversion_params, 0, sizeof(color_conversion_params)); color_conversion_params.color_range = arg_parser.settings.color_range; color_conversion_params.egl = &egl; - color_conversion_params.load_external_image_shader = any_video_sources_uses_external_image(video_sources); + color_conversion_params.load_external_image_shader = gsr_video_sources_uses_external_image(video_sources); gsr_video_encoder_get_textures(video_encoder, color_conversion_params.destination_textures, color_conversion_params.destination_textures_size, &color_conversion_params.num_destination_textures, &color_conversion_params.destination_color); gsr_color_conversion color_conversion; @@ -3044,43 +2285,45 @@ int main(int argc, char **argv) { bool use_damage_tracking = false; gsr_damage damage; memset(&damage, 0, sizeof(damage)); - if(arg_parser.settings.framerate_mode == GSR_FRAMERATE_MODE_CONTENT && is_capturing_damage_tracked_target(capture_sources)) { + if(arg_parser.settings.framerate_mode == GSR_FRAMERATE_MODE_CONTENT && gsr_capture_sources_has_damage_tracked_target(capture_sources)) { if(gsr_window_get_display_server(window) == GSR_DISPLAY_SERVER_X11) { - gsr_damage_init(&damage, &egl, &x11_cursor, arg_parser.settings.record_cursor); + gsr_damage_init(&damage, &egl, &capture_deps.x11_cursor, arg_parser.settings.record_cursor); use_damage_tracking = true; - for(const CaptureSource &capture_source : capture_sources) { - switch(capture_source.type) { + for(size_t i = 0; i < capture_sources->num_items; ++i) { + const gsr_capture_source *capture_source = &capture_sources->items[i]; + switch(capture_source->type) { case GSR_CAPTURE_SOURCE_TYPE_WINDOW: - gsr_damage_start_tracking_window(&damage, capture_source.window_id); + gsr_damage_start_tracking_window(&damage, capture_source->window_id); break; case GSR_CAPTURE_SOURCE_TYPE_MONITOR: case GSR_CAPTURE_SOURCE_TYPE_REGION: // TODO: When capturing a region only track damage in that region - gsr_damage_start_tracking_monitor(&damage, capture_source.name.c_str()); + gsr_damage_start_tracking_monitor(&damage, capture_source->name); break; default: break; } } - } else if(is_capturing_monitor_or_region(capture_sources)) { + } else if(gsr_capture_sources_has_monitor_or_region(capture_sources)) { gsr_log(GSR_LOG_LEVEL_WARNING, "\"-fm content\" has no effect on Wayland when recording a monitor. Either record a monitor on X11 or capture with desktop portal instead (-w portal)"); } } while(running) { while(gsr_window_process_event(window)) { - if(x11_cursor_display && arg_parser.settings.record_cursor) - gsr_cursor_on_event(&x11_cursor, gsr_window_get_event_data(window)); + if(capture_deps.x11_cursor_display && arg_parser.settings.record_cursor) + gsr_cursor_on_event(&capture_deps.x11_cursor, gsr_window_get_event_data(window)); gsr_damage_on_event(&damage, gsr_window_get_event_data(window)); - for(VideoSource &video_source : video_sources) { + for(size_t video_source_index = 0; video_source_index < video_sources->num_items; ++video_source_index) { + gsr_video_source &video_source = video_sources->items[video_source_index]; gsr_capture_on_event(video_source.capture, &egl); } } - if(x11_cursor_display && arg_parser.settings.record_cursor) - gsr_cursor_tick(&x11_cursor, DefaultRootWindow(x11_cursor_display)); + if(capture_deps.x11_cursor_display && arg_parser.settings.record_cursor) + gsr_cursor_tick(&capture_deps.x11_cursor, DefaultRootWindow(capture_deps.x11_cursor_display)); gsr_damage_tick(&damage); @@ -3090,7 +2333,8 @@ int main(int argc, char **argv) { if(use_damage_tracking) damaged = gsr_damage_is_damaged(&damage); - for(VideoSource &video_source : video_sources) { + for(size_t video_source_index = 0; video_source_index < video_sources->num_items; ++video_source_index) { + gsr_video_source &video_source = video_sources->items[video_source_index]; gsr_capture_tick(video_source.capture); if(gsr_capture_should_stop(video_source.capture, &should_stop_error)) { @@ -3149,15 +2393,13 @@ int main(int argc, char **argv) { gsr_damage_clear(&damage); gsr_plugins_clear_damage(&plugins); - gsr_capture_kms_cleanup_kms_fds(); + gsr_capture_deps_cleanup_kms_fds(&capture_deps); - if(kms_client_initialized) { - if(gsr_kms_client_get_kms(&kms_client, &kms_response) != 0) - gsr_log(GSR_LOG_LEVEL_ERROR, "failed to get kms, error: %d (%s)", kms_response.result, kms_response.err_msg); - } + gsr_capture_deps_update_kms(&capture_deps); bool capture_has_synchronous_task = false; - for(VideoSource &video_source : video_sources) { + for(size_t video_source_index = 0; video_source_index < video_sources->num_items; ++video_source_index) { + gsr_video_source &video_source = video_sources->items[video_source_index]; if(video_source.capture->clear_damage) video_source.capture->clear_damage(video_source.capture); @@ -3170,7 +2412,8 @@ int main(int argc, char **argv) { } } - for(VideoSource &video_source : video_sources) { + for(size_t video_source_index = 0; video_source_index < video_sources->num_items; ++video_source_index) { + gsr_video_source &video_source = video_sources->items[video_source_index]; if(video_source.capture->pre_capture) video_source.capture->pre_capture(video_source.capture, &video_source.metadata, output_color_conversion); } @@ -3180,11 +2423,12 @@ int main(int argc, char **argv) { gsr_color_conversion_clear(output_color_conversion); } - for(VideoSource &video_source : video_sources) { + for(size_t video_source_index = 0; video_source_index < video_sources->num_items; ++video_source_index) { + gsr_video_source &video_source = video_sources->items[video_source_index]; gsr_capture_capture(video_source.capture, &video_source.metadata, output_color_conversion); } - gsr_capture_kms_cleanup_kms_fds(); + gsr_capture_deps_cleanup_kms_fds(&capture_deps); if(plugins.num_plugins > 0) { gsr_plugins_draw(&plugins); @@ -3202,7 +2446,8 @@ int main(int argc, char **argv) { gsr_egl_swap_buffers(&egl); gsr_video_encoder_copy_textures_to_frame(video_encoder, video_frame, output_color_conversion); - for(VideoSource &video_source : video_sources) { + for(size_t video_source_index = 0; video_source_index < video_sources->num_items; ++video_source_index) { + gsr_video_source &video_source = video_sources->items[video_source_index]; if(hdr && !hdr_metadata_set && !arg_parser.settings.is_replaying && add_hdr_metadata_to_video_stream(video_source.capture, video_stream)) hdr_metadata_set = true; } @@ -3421,23 +2666,15 @@ int main(int argc, char **argv) { avformat_free_context(av_format_context); } - gsr_cursor_deinit(&x11_cursor); gsr_damage_deinit(&damage); gsr_color_conversion_deinit(&color_conversion); gsr_video_encoder_destroy(video_encoder, video_codec_context); gsr_encoder_deinit(&encoder); - for(VideoSource &video_source : video_sources) { - gsr_capture_destroy(video_source.capture); - } + gsr_video_sources_deinit(video_sources); #ifdef GSR_APP_AUDIO gsr_pipewire_audio_deinit(&pipewire_audio); #endif - if(kms_client_initialized) { - gsr_capture_kms_cleanup_kms_fds(); - gsr_kms_client_deinit(&kms_client); - } - - gsr_kde_night_light_destroy(kde_night_light); + gsr_capture_deps_deinit(&capture_deps); if(!arg_parser.settings.is_replaying && arg_parser.settings.recording_saved_script) run_recording_saved_script_async(arg_parser.settings.recording_saved_script, arg_parser.settings.filename, "regular"); diff --git a/src/recorder/capture_setup.c b/src/recorder/capture_setup.c new file mode 100644 index 0000000..93dbe37 --- /dev/null +++ b/src/recorder/capture_setup.c @@ -0,0 +1,543 @@ +#include "../../include/recorder/capture_setup.h" +#include "../../include/recorder/error.h" +#include "../../include/recorder/windowing.h" +#include "../../include/capture/nvfbc.h" +#include "../../include/capture/xcomposite.h" +#include "../../include/capture/ximage.h" +#include "../../include/capture/kms.h" +#include "../../include/capture/v4l2.h" +#ifdef GSR_PORTAL +#include "../../include/capture/portal.h" +#endif +#include "../../include/args_parser.h" +#include "../../include/utils.h" +#include "../../include/window/window.h" +#include "../../include/log.h" + +#include +#include +#include +#include + +typedef struct { + gsr_window *window; +} monitor_output_callback_userdata; + +typedef struct { + char *output_name; +} first_output_callback_userdata; + +typedef struct { + gsr_window *window; + vec2i position; + char *output_name; + vec2i monitor_pos; + vec2i monitor_size; + double monitor_scale_inverted; +} monitor_by_position_callback_userdata; + +static void monitor_output_callback_print(const gsr_monitor *monitor, void *userdata) { + const monitor_output_callback_userdata *options = userdata; + vec2i monitor_position = monitor->pos; + vec2i monitor_size = monitor->size; + if(gsr_window_get_display_server(options->window) == GSR_DISPLAY_SERVER_WAYLAND) { + gsr_monitor_rotation monitor_rotation = GSR_MONITOR_ROT_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; + } + } + fprintf(stderr, " \"%.*s\" (%dx%d+%d+%d)\n", monitor->name_len, monitor->name, monitor_size.x, monitor_size.y, monitor_position.x, monitor_position.y); +} + +static void monitor_output_callback_print_region(const gsr_monitor *monitor, void *userdata) { + (void)userdata; + const vec2i monitor_position = monitor->logical_pos; + const vec2i monitor_size = monitor->logical_size; + fprintf(stderr, " \"%.*s\" (%dx%d+%d+%d)\n", monitor->name_len, monitor->name, monitor_size.x, monitor_size.y, monitor_position.x, monitor_position.y); +} + +static void get_first_output_callback(const gsr_monitor *monitor, void *userdata) { + first_output_callback_userdata *data = userdata; + if(!data->output_name) + data->output_name = strdup(monitor->name); +} + +static void get_monitor_by_position_callback(const gsr_monitor *monitor, void *userdata) { + monitor_by_position_callback_userdata *data = userdata; + + const vec2i monitor_position = monitor->logical_pos; + const vec2i monitor_size = monitor->size; + const vec2i monitor_logical_size = monitor->logical_size; + + if(!data->output_name && data->position.x >= monitor_position.x && data->position.x <= monitor_position.x + monitor_logical_size.x + && data->position.y >= monitor_position.y && data->position.y <= monitor_position.y + monitor_logical_size.y) + { + data->output_name = strdup(monitor->name); + data->monitor_pos = monitor_position; + data->monitor_size = monitor_size; + data->monitor_scale_inverted = (double)monitor_size.x / (double)monitor_logical_size.x; + } +} + +void gsr_capture_deps_init(gsr_capture_deps *self) { + memset(self, 0, sizeof(*self)); +} + +void gsr_capture_deps_init_cursor(gsr_capture_deps *self, gsr_egl *egl, bool record_cursor) { + if(gsr_window_get_display_server(egl->window) != GSR_DISPLAY_SERVER_X11 || !record_cursor) + return; + + self->x11_cursor_display = (Display*)gsr_window_get_display(egl->window); + gsr_cursor_init(&self->x11_cursor, egl, self->x11_cursor_display); +} + +void gsr_capture_deps_deinit(gsr_capture_deps *self) { + gsr_cursor_deinit(&self->x11_cursor); + self->x11_cursor_display = NULL; + + if(self->kms_client_initialized) { + gsr_capture_deps_cleanup_kms_fds(self); + gsr_kms_client_deinit(&self->kms_client); + self->kms_client_initialized = false; + } + + gsr_kde_night_light_destroy(self->kde_night_light); + self->kde_night_light = NULL; + self->kde_night_light_initialized = false; +} + +void gsr_capture_deps_cleanup_kms_fds(gsr_capture_deps *self) { + for(int i = 0; i < self->kms_response.num_items; ++i) { + for(int j = 0; j < self->kms_response.items[i].num_dma_bufs; ++j) { + gsr_kms_response_dma_buf *dma_buf = &self->kms_response.items[i].dma_buf[j]; + if(dma_buf->fd > 0) { + close(dma_buf->fd); + dma_buf->fd = 0; + } + } + self->kms_response.items[i].num_dma_bufs = 0; + } + self->kms_response.num_items = 0; +} + +void gsr_capture_deps_update_kms(gsr_capture_deps *self) { + if(!self->kms_client_initialized) + return; + + if(gsr_kms_client_get_kms(&self->kms_client, &self->kms_response) != 0) + gsr_log(GSR_LOG_LEVEL_ERROR, "failed to get kms, error: %d (%s)", self->kms_response.result, self->kms_response.err_msg); +} + +static int validate_monitor_get_valid(const gsr_egl *egl, const char *window, char *output_name, size_t output_name_size) { + const bool is_x11 = gsr_window_get_display_server(egl->window) == GSR_DISPLAY_SERVER_X11; + const gsr_connection_type connection_type = is_x11 ? GSR_CONNECTION_X11 : GSR_CONNECTION_DRM; + const bool capture_use_drm = monitor_capture_use_drm(egl->window, egl->gpu_info.vendor); + + snprintf(output_name, output_name_size, "%s", window); + if(strcmp(output_name, "screen") == 0) { + first_output_callback_userdata data; + data.output_name = NULL; + for_each_active_monitor_output(egl->window, egl->card_path, connection_type, get_first_output_callback, &data); + + if(data.output_name) { + snprintf(output_name, output_name_size, "%s", data.output_name); + free(data.output_name); + } else { + gsr_log(GSR_LOG_LEVEL_ERROR, "no usable output found"); + return GSR_ERROR_MONITOR_NOT_FOUND; + } + } else if(capture_use_drm || (strcmp(output_name, "screen-direct") != 0 && strcmp(output_name, "screen-direct-force") != 0)) { + gsr_monitor gmon; + if(!get_monitor_by_name(egl, connection_type, output_name, &gmon)) { + gsr_log(GSR_LOG_LEVEL_ERROR, "display \"%s\" not found, expected one of:", output_name); + fprintf(stderr, " \"screen\"\n"); + if(!capture_use_drm) + fprintf(stderr, " \"screen-direct\"\n"); + + monitor_output_callback_userdata userdata; + userdata.window = egl->window; + for_each_active_monitor_output(egl->window, egl->card_path, connection_type, monitor_output_callback_print, &userdata); + return GSR_ERROR_MONITOR_NOT_FOUND; + } + } + + return GSR_ERROR_OK; +} + +static bool get_monitor_by_region_center(const gsr_egl *egl, vec2i region_position, vec2i region_size, char *output_name, size_t output_name_size, vec2i *monitor_pos, vec2i *monitor_size, double *monitor_scale_inverted) { + const bool is_x11 = gsr_window_get_display_server(egl->window) == GSR_DISPLAY_SERVER_X11; + const gsr_connection_type connection_type = is_x11 ? GSR_CONNECTION_X11 : GSR_CONNECTION_WAYLAND; + + monitor_by_position_callback_userdata data; + data.window = egl->window; + data.position = (vec2i){ region_position.x + region_size.x / 2, region_position.y + region_size.y / 2 }; + data.output_name = NULL; + data.monitor_pos = (vec2i){0, 0}; + data.monitor_size = (vec2i){0, 0}; + data.monitor_scale_inverted = 1.0; + for_each_active_monitor_output(egl->window, egl->card_path, connection_type, get_monitor_by_position_callback, &data); + + output_name[0] = '\0'; + if(data.output_name) { + snprintf(output_name, output_name_size, "%s", data.output_name); + free(data.output_name); + } + *monitor_pos = data.monitor_pos; + *monitor_size = data.monitor_size; + *monitor_scale_inverted = data.monitor_scale_inverted; + return output_name[0] != '\0'; +} + +static int region_get_data(gsr_egl *egl, vec2i *region_size, vec2i *region_position, char *output_name, size_t output_name_size) { + vec2i monitor_pos = {0, 0}; + vec2i monitor_size = {0, 0}; + double monitor_scale_inverted = 1.0; + if(!get_monitor_by_region_center(egl, *region_position, *region_size, output_name, output_name_size, &monitor_pos, &monitor_size, &monitor_scale_inverted)) { + const bool is_x11 = gsr_window_get_display_server(egl->window) == GSR_DISPLAY_SERVER_X11; + const gsr_connection_type connection_type = is_x11 ? GSR_CONNECTION_X11 : GSR_CONNECTION_WAYLAND; + gsr_log(GSR_LOG_LEVEL_ERROR, "the region %dx%d+%d+%d doesn't match any monitor. Available monitors and their regions:", region_size->x, region_size->y, region_position->x, region_position->y); + for_each_active_monitor_output(egl->window, egl->card_path, connection_type, monitor_output_callback_print_region, NULL); + return GSR_ERROR_MONITOR_NOT_FOUND; + } + + /* Capture whole monitor when region size is set to 0x0 */ + if(region_size->x == 0 && region_size->y == 0) { + region_position->x = 0; + region_position->y = 0; + } else { + region_position->x -= monitor_pos.x; + region_position->y -= monitor_pos.y; + /* Match drm plane coordinate space (1x scaling) to wayland coordinate space (which may have scaling set by user) */ + region_position->x *= monitor_scale_inverted; + region_position->y *= monitor_scale_inverted; + + region_size->x *= monitor_scale_inverted; + region_size->y *= monitor_scale_inverted; + } + + return GSR_ERROR_OK; +} + +static gsr_capture* create_monitor_capture(const gsr_recorder_settings *settings, gsr_egl *egl, gsr_capture_deps *deps, const gsr_capture_source *capture_source, bool prefer_ximage, int *error) { + *error = GSR_ERROR_OK; + + if(gsr_window_get_display_server(egl->window) == GSR_DISPLAY_SERVER_X11 && prefer_ximage) { + gsr_capture_ximage_params ximage_params; + memset(&ximage_params, 0, sizeof(ximage_params)); + ximage_params.egl = egl; + ximage_params.cursor = &deps->x11_cursor; + ximage_params.display_to_capture = capture_source->name; + ximage_params.record_cursor = settings->record_cursor; + ximage_params.output_resolution = settings->output_resolution; + ximage_params.region_size = capture_source->region_size; + ximage_params.region_position = capture_source->region_pos; + return gsr_capture_ximage_create(&ximage_params); + } + + if(monitor_capture_use_drm(egl->window, egl->gpu_info.vendor)) { + if(!deps->kms_client_initialized) { + deps->kms_client_initialized = true; + const int kms_init_res = gsr_kms_client_init(&deps->kms_client, egl->card_path); + if(kms_init_res != 0) { + *error = kms_init_res < 0 ? GSR_ERROR_GENERIC : -kms_init_res; + return NULL; + } + } + + if(!deps->kde_night_light_initialized && gsr_window_get_display_server(egl->window) == GSR_DISPLAY_SERVER_WAYLAND) { + deps->kde_night_light_initialized = true; + deps->kde_night_light = gsr_kde_night_light_create(); + } + + gsr_capture_kms_params kms_params; + memset(&kms_params, 0, sizeof(kms_params)); + kms_params.egl = egl; + kms_params.x11_cursor = &deps->x11_cursor; + kms_params.kms_response = &deps->kms_response; + kms_params.kde_night_light = deps->kde_night_light; + kms_params.display_to_capture = capture_source->name; + kms_params.record_cursor = settings->record_cursor; + kms_params.hdr = video_codec_is_hdr(settings->video_codec); + kms_params.fps = settings->fps; + kms_params.output_resolution = settings->output_resolution; + kms_params.region_size = capture_source->region_size; + kms_params.region_position = capture_source->region_pos; + return gsr_capture_kms_create(&kms_params); + } else { + const char *capture_source_real = capture_source->name; + const bool direct_capture = strcmp(capture_source->name, "screen-direct") == 0 || strcmp(capture_source->name, "screen-direct-force") == 0; + if(direct_capture) { + capture_source_real = "screen"; + gsr_log(GSR_LOG_LEVEL_WARNING, "%s capture option is not recommended unless you use G-SYNC as Nvidia has driver issues that can cause your system or games to freeze/crash.", capture_source->name); + } + + gsr_capture_nvfbc_params nvfbc_params; + memset(&nvfbc_params, 0, sizeof(nvfbc_params)); + nvfbc_params.egl = egl; + nvfbc_params.display_to_capture = capture_source_real; + nvfbc_params.fps = settings->fps; + nvfbc_params.direct_capture = direct_capture; + nvfbc_params.record_cursor = settings->record_cursor; + nvfbc_params.output_resolution = settings->output_resolution; + nvfbc_params.region_size = capture_source->region_size; + nvfbc_params.region_position = capture_source->region_pos; + return gsr_capture_nvfbc_create(&nvfbc_params); + } +} + +static gsr_capture* create_capture_impl(const gsr_recorder_settings *settings, gsr_egl *egl, gsr_capture_deps *deps, gsr_capture_source *capture_source, bool prefer_ximage, int *error) { + bool follow_focused = false; + const bool wayland = gsr_window_get_display_server(egl->window) == GSR_DISPLAY_SERVER_WAYLAND; + + *error = GSR_ERROR_OK; + gsr_capture *capture = NULL; + if(capture_source->type == GSR_CAPTURE_SOURCE_TYPE_FOCUSED_WINDOW) { + if(wayland) { + gsr_log(GSR_LOG_LEVEL_ERROR, "GPU Screen Recorder window capture only works in a pure X11 session. Xwayland is not supported. You can record a monitor instead on wayland"); + *error = GSR_ERROR_UNSUPPORTED; + return NULL; + } + + if(settings->output_resolution.x <= 0 || settings->output_resolution.y <= 0) { + gsr_log(GSR_LOG_LEVEL_ERROR, "invalid value for option -s '%dx%d' when using -w focused option. expected width and height to be greater than 0", settings->output_resolution.x, settings->output_resolution.y); + args_parser_print_usage(); + *error = GSR_ERROR_GENERIC; + return NULL; + } + + follow_focused = true; + } else if(capture_source->type == GSR_CAPTURE_SOURCE_TYPE_PORTAL) { +#ifdef GSR_PORTAL + /* Desktop portal capture on x11 doesn't seem to be hardware accelerated */ + if(!wayland) { + gsr_log(GSR_LOG_LEVEL_ERROR, "desktop portal capture is not supported on X11"); + *error = GSR_ERROR_GENERIC; + return NULL; + } + + gsr_capture_portal_params portal_params; + memset(&portal_params, 0, sizeof(portal_params)); + portal_params.egl = egl; + portal_params.record_cursor = settings->record_cursor; + portal_params.restore_portal_session = settings->restore_portal_session; + portal_params.portal_session_token_filepath = settings->portal_session_token_filepath; + portal_params.output_resolution = settings->output_resolution; + capture = gsr_capture_portal_create(&portal_params); + if(!capture) { + *error = GSR_ERROR_GENERIC; + return NULL; + } +#else + gsr_log(GSR_LOG_LEVEL_ERROR, "option '-w portal' used but GPU Screen Recorder was compiled without desktop portal support. Please recompile GPU Screen recorder with the -Dportal=true option"); + *error = GSR_ERROR_UNSUPPORTED; + return NULL; +#endif + } else if(capture_source->type == GSR_CAPTURE_SOURCE_TYPE_REGION) { + const int region_result = region_get_data(egl, &capture_source->region_size, &capture_source->region_pos, capture_source->name, sizeof(capture_source->name)); + if(region_result != GSR_ERROR_OK) { + *error = region_result; + return NULL; + } + + capture = create_monitor_capture(settings, egl, deps, capture_source, prefer_ximage, error); + if(!capture) { + if(*error == GSR_ERROR_OK) + *error = GSR_ERROR_GENERIC; + return NULL; + } + } else if(capture_source->type == GSR_CAPTURE_SOURCE_TYPE_MONITOR) { + char monitor_name[GSR_CAPTURE_SOURCE_NAME_MAX_SIZE]; + const int monitor_result = validate_monitor_get_valid(egl, capture_source->name, monitor_name, sizeof(monitor_name)); + if(monitor_result != GSR_ERROR_OK) { + *error = monitor_result; + return NULL; + } + snprintf(capture_source->name, sizeof(capture_source->name), "%s", monitor_name); + + capture = create_monitor_capture(settings, egl, deps, capture_source, prefer_ximage, error); + if(!capture) { + if(*error == GSR_ERROR_OK) + *error = GSR_ERROR_GENERIC; + return NULL; + } + } else if(capture_source->type == GSR_CAPTURE_SOURCE_TYPE_V4L2) { + gsr_capture_v4l2_params v4l2_params; + memset(&v4l2_params, 0, sizeof(v4l2_params)); + v4l2_params.egl = egl; + v4l2_params.output_resolution = settings->output_resolution; + v4l2_params.device_path = capture_source->name; + v4l2_params.pixfmt = capture_source->v4l2_pixfmt; + v4l2_params.camera_fps = capture_source->camera_fps; + v4l2_params.camera_resolution.width = capture_source->camera_resolution.x; + v4l2_params.camera_resolution.height = capture_source->camera_resolution.y; + capture = gsr_capture_v4l2_create(&v4l2_params); + if(!capture) { + *error = GSR_ERROR_GENERIC; + return NULL; + } + } else { + if(wayland) { + gsr_log(GSR_LOG_LEVEL_ERROR, "GPU Screen Recorder window capture only works in a pure X11 session. Xwayland is not supported. You can record a monitor instead on wayland or use -w portal option which supports window capture if your wayland compositor supports window capture"); + *error = GSR_ERROR_UNSUPPORTED; + return NULL; + } + } + + if(!capture) { + gsr_capture_xcomposite_params xcomposite_params; + memset(&xcomposite_params, 0, sizeof(xcomposite_params)); + xcomposite_params.egl = egl; + xcomposite_params.cursor = &deps->x11_cursor; + xcomposite_params.window = capture_source->window_id; + xcomposite_params.follow_focused = follow_focused; + xcomposite_params.record_cursor = settings->record_cursor; + xcomposite_params.output_resolution = settings->output_resolution; + capture = gsr_capture_xcomposite_create(&xcomposite_params); + if(!capture) { + *error = GSR_ERROR_GENERIC; + return NULL; + } + } + + return capture; +} + +int gsr_video_sources_create(gsr_video_sources *self, const gsr_recorder_settings *settings, gsr_egl *egl, gsr_capture_deps *deps, bool prefer_ximage, gsr_capture_sources *capture_sources, vec2i *video_size) { + memset(self, 0, sizeof(*self)); + if(capture_sources->num_items == 0) + return GSR_ERROR_GENERIC; + + self->items = calloc(capture_sources->num_items, sizeof(gsr_video_source)); + if(!self->items) { + gsr_log(GSR_LOG_LEVEL_ERROR, "failed to allocate video sources"); + return GSR_ERROR_GENERIC; + } + + for(size_t i = 0; i < capture_sources->num_items; ++i) { + gsr_capture_source *capture_source = &capture_sources->items[i]; + gsr_video_source *video_source = &self->items[i]; + + memset(&video_source->metadata, 0, sizeof(video_source->metadata)); + video_source->metadata.fps = settings->fps; + video_source->metadata.halign = capture_source->halign; + video_source->metadata.valign = capture_source->valign; + video_source->metadata.flip = (gsr_flip)capture_source->flip; + video_source->capture_source = capture_source; + + int error = GSR_ERROR_OK; + video_source->capture = create_capture_impl(settings, egl, deps, capture_source, prefer_ximage, &error); + if(!video_source->capture) { + gsr_video_sources_deinit(self); + return error; + } + + ++self->num_items; + } + + for(size_t i = 0; i < self->num_items; ++i) { + const int capture_result = gsr_capture_start(self->items[i].capture, &self->items[i].metadata); + if(capture_result != 0) { + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_capture_start failed"); + gsr_video_sources_deinit(self); + return -capture_result; + } + } + + vec2i start_pos = {99999, 99999}; + vec2i end_pos = {-99999, -99999}; + for(size_t i = 0; i < self->num_items; ++i) { + const gsr_video_source *video_source = &self->items[i]; + /* TODO: Skip scalar positions for now, but this should be handled in a better way. + Maybe handle scalars at the next loop by multiplying video size by the scalar. */ + if(video_source->capture_source->pos.x_type == VVEC2I_TYPE_SCALAR || video_source->capture_source->pos.y_type == VVEC2I_TYPE_SCALAR + || (video_source->capture_source->size.x_type == VVEC2I_TYPE_SCALAR && video_source->capture_source->size.x != 100) + || (video_source->capture_source->size.y_type == VVEC2I_TYPE_SCALAR && video_source->capture_source->size.y != 100)) + { + continue; + } + const vec2i video_source_start_pos = {video_source->capture_source->pos.x, video_source->capture_source->pos.y}; + const vec2i video_source_end_pos = {video_source_start_pos.x + video_source->metadata.video_size.x, video_source_start_pos.y + video_source->metadata.video_size.y}; + + if(video_source_start_pos.x < start_pos.x) + start_pos.x = video_source_start_pos.x; + if(video_source_start_pos.y < start_pos.y) + start_pos.y = video_source_start_pos.y; + + if(video_source_end_pos.x > end_pos.x) + end_pos.x = video_source_end_pos.x; + if(video_source_end_pos.y > end_pos.y) + end_pos.y = video_source_end_pos.y; + } + + video_size->x = end_pos.x - start_pos.x; + video_size->y = end_pos.y - start_pos.y; + if(video_size->x < 0) + video_size->x = 0; + if(video_size->y < 0) + video_size->y = 0; + + for(size_t i = 0; i < self->num_items; ++i) { + self->items[i].metadata.video_size = *video_size; + } + + return GSR_ERROR_OK; +} + +void gsr_video_sources_update_with_real_video_size(gsr_video_sources *self, vec2i video_size) { + for(size_t i = 0; i < self->num_items; ++i) { + gsr_video_source *video_source = &self->items[i]; + const gsr_capture_source *capture_source = video_source->capture_source; + + video_source->metadata.recording_size = video_source->metadata.video_size; + /* TODO: What if this updated resolution is above max resolution? */ + video_source->metadata.video_size = video_size; + + if(capture_source->pos.x != 0 || capture_source->pos.y != 0) { + video_source->metadata.position.x = capture_source->pos.x; + video_source->metadata.position.y = capture_source->pos.y; + + if(capture_source->pos.x_type == VVEC2I_TYPE_SCALAR) + video_source->metadata.position.x = video_source->metadata.video_size.x * ((double)video_source->metadata.position.x / 100.0); + + if(capture_source->pos.y_type == VVEC2I_TYPE_SCALAR) + video_source->metadata.position.y = video_source->metadata.video_size.y * ((double)video_source->metadata.position.y / 100.0); + } + + if(capture_source->size.x != 0 || capture_source->size.y != 0) { + video_source->metadata.recording_size.x = capture_source->size.x; + video_source->metadata.recording_size.y = capture_source->size.y; + + if(capture_source->size.x_type == VVEC2I_TYPE_SCALAR) + video_source->metadata.recording_size.x = video_source->metadata.video_size.x * ((double)video_source->metadata.recording_size.x / 100.0); + + if(capture_source->size.y_type == VVEC2I_TYPE_SCALAR) + video_source->metadata.recording_size.y = video_source->metadata.video_size.y * ((double)video_source->metadata.recording_size.y / 100.0); + } + } +} + +bool gsr_video_sources_uses_external_image(const gsr_video_sources *self) { + for(size_t i = 0; i < self->num_items; ++i) { + if(gsr_capture_uses_external_image(self->items[i].capture)) + return true; + } + return false; +} + +void gsr_video_sources_deinit(gsr_video_sources *self) { + for(size_t i = 0; i < self->num_items; ++i) { + if(self->items[i].capture) { + gsr_capture_destroy(self->items[i].capture); + self->items[i].capture = NULL; + } + } + + if(self->items) { + free(self->items); + self->items = NULL; + } + self->num_items = 0; +} diff --git a/src/recorder/capture_source.c b/src/recorder/capture_source.c new file mode 100644 index 0000000..5407abc --- /dev/null +++ b/src/recorder/capture_source.c @@ -0,0 +1,390 @@ +#include "../../include/recorder/capture_source.h" +#include "../../include/recorder/error.h" +#include "../../include/utils.h" +#include "../../include/log.h" + +#include +#include +#include + +typedef struct { + gsr_capture_source *capture_source; + bool is_first_column; + int error; +} parse_capture_source_options_userdata; + +typedef struct { + gsr_capture_sources *capture_sources; + vec2i region_position; + vec2i region_size; + bool has_multiple_capture_sources; + int error; +} parse_capture_source_arg_userdata; + +void gsr_capture_source_init(gsr_capture_source *self, vec2i region_position, vec2i region_size) { + memset(self, 0, sizeof(*self)); + self->type = GSR_CAPTURE_SOURCE_TYPE_WINDOW; + self->halign = GSR_CAPTURE_ALIGN_CENTER; + self->valign = GSR_CAPTURE_ALIGN_CENTER; + self->v4l2_pixfmt = GSR_CAPTURE_V4L2_PIXFMT_AUTO; + self->flip = GSR_FLIP_NONE; + self->pos = (vvec2i){0, 0, VVEC2I_TYPE_PIXELS, VVEC2I_TYPE_PIXELS}; + self->size = (vvec2i){100, 100, VVEC2I_TYPE_SCALAR, VVEC2I_TYPE_SCALAR}; + self->region_pos = region_position; + self->region_size = region_size; +} + +static bool is_hex_num(char c) { + return (c >= 'A' && c <= 'F') || (c >= 'a' && c <= 'f') || (c >= '0' && c <= '9'); +} + +static bool contains_non_hex_number(const char *str) { + bool hex_start = false; + size_t len = strlen(str); + if(len >= 2 && memcmp(str, "0x", 2) == 0) { + str += 2; + len -= 2; + hex_start = true; + } + + bool is_hex = false; + for(size_t i = 0; i < len; ++i) { + char c = str[i]; + if(c == '\0') + return false; + if(!is_hex_num(c)) + return true; + if((c >= 'A' && c <= 'F') || (c >= 'a' && c <= 'f')) + is_hex = true; + } + + return is_hex && !hex_start; +} + +static void capture_source_type_from_string(const char *capture_source_str, size_t size, gsr_capture_source *capture_source) { + char capture_source_str_n[64]; + snprintf(capture_source_str_n, sizeof(capture_source_str_n), "%.*s", (int)size, capture_source_str); + + if(size == 7 && memcmp(capture_source_str_n, "focused", 7) == 0) { + capture_source->type = GSR_CAPTURE_SOURCE_TYPE_FOCUSED_WINDOW; + } else if(size == 6 && memcmp(capture_source_str_n, "portal", 6) == 0) { + capture_source->type = GSR_CAPTURE_SOURCE_TYPE_PORTAL; + } else if(size == 6 && memcmp(capture_source_str_n, "region", 6) == 0) { + capture_source->type = GSR_CAPTURE_SOURCE_TYPE_REGION; + } else if(size >= 10 && memcmp(capture_source_str_n, "/dev/video", 10) == 0) { + capture_source->type = GSR_CAPTURE_SOURCE_TYPE_V4L2; + } else if(sscanf(capture_source_str_n, "%dx%d+%d+%d", &capture_source->region_size.x, &capture_source->region_size.y, &capture_source->region_pos.x, &capture_source->region_pos.y) == 4) { + capture_source->type = GSR_CAPTURE_SOURCE_TYPE_REGION; + capture_source->region_set = true; + } else if(contains_non_hex_number(capture_source_str_n)) { + capture_source->type = GSR_CAPTURE_SOURCE_TYPE_MONITOR; + } else { + capture_source->type = GSR_CAPTURE_SOURCE_TYPE_WINDOW; + } +} + +static bool string_to_capture_alignment(const char *str, size_t len, gsr_capture_alignment *alignment) { + if(len == 5 && memcmp(str, "start", 5) == 0) { + *alignment = GSR_CAPTURE_ALIGN_START; + return true; + } else if(len == 6 && memcmp(str, "center", 6) == 0) { + *alignment = GSR_CAPTURE_ALIGN_CENTER; + return true; + } else if(len == 3 && memcmp(str, "end", 3) == 0) { + *alignment = GSR_CAPTURE_ALIGN_END; + return true; + } else { + return false; + } +} + +static bool string_to_v4l2_pixfmt(const char *str, size_t len, gsr_capture_v4l2_pixfmt *pixfmt) { + if(len == 4 && memcmp(str, "auto", 4) == 0) { + *pixfmt = GSR_CAPTURE_V4L2_PIXFMT_AUTO; + return true; + } else if(len == 4 && memcmp(str, "yuyv", 4) == 0) { + *pixfmt = GSR_CAPTURE_V4L2_PIXFMT_YUYV; + return true; + } else if(len == 5 && memcmp(str, "mjpeg", 5) == 0) { + *pixfmt = GSR_CAPTURE_V4L2_PIXFMT_MJPEG; + return true; + } else { + return false; + } +} + +static bool string_to_bool(const char *str, size_t len, bool *value) { + if(len == 4 && memcmp(str, "true", 4) == 0) { + *value = true; + return true; + } else if(len == 5 && memcmp(str, "false", 5) == 0) { + *value = false; + return true; + } else { + return false; + } +} + +static bool parse_capture_source_options_callback(const char *sub, size_t size, void *userdata) { + parse_capture_source_options_userdata *parse_userdata = userdata; + gsr_capture_source *capture_source = parse_userdata->capture_source; + if(size == 0) + return true; + + /* First column contains the capture target */ + if(parse_userdata->is_first_column) { + parse_userdata->is_first_column = false; + return true; + } + + if(gsr_string_starts_with(sub, size, "x=")) { + capture_source->pos.x_type = sub[size - 1] == '%' ? VVEC2I_TYPE_SCALAR : VVEC2I_TYPE_PIXELS; + sub += 2; + size -= 2; + if(!gsr_string_to_int(sub, size, &capture_source->pos.x)) { + gsr_log(GSR_LOG_LEVEL_ERROR, "invalid capture target value for option x: \"%.*s\", expected a number", (int)size, sub); + parse_userdata->error = GSR_ERROR_GENERIC; + return false; + } + } else if(gsr_string_starts_with(sub, size, "y=")) { + capture_source->pos.y_type = sub[size - 1] == '%' ? VVEC2I_TYPE_SCALAR : VVEC2I_TYPE_PIXELS; + sub += 2; + size -= 2; + if(!gsr_string_to_int(sub, size, &capture_source->pos.y)) { + gsr_log(GSR_LOG_LEVEL_ERROR, "invalid capture target value for option y: \"%.*s\", expected a number", (int)size, sub); + parse_userdata->error = GSR_ERROR_GENERIC; + return false; + } + } else if(gsr_string_starts_with(sub, size, "width=")) { + capture_source->size.x_type = sub[size - 1] == '%' ? VVEC2I_TYPE_SCALAR : VVEC2I_TYPE_PIXELS; + sub += 6; + size -= 6; + if(!gsr_string_to_int(sub, size, &capture_source->size.x)) { + gsr_log(GSR_LOG_LEVEL_ERROR, "invalid capture target value for option width: \"%.*s\", expected a number", (int)size, sub); + parse_userdata->error = GSR_ERROR_GENERIC; + return false; + } + } else if(gsr_string_starts_with(sub, size, "height=")) { + capture_source->size.y_type = sub[size - 1] == '%' ? VVEC2I_TYPE_SCALAR : VVEC2I_TYPE_PIXELS; + sub += 7; + size -= 7; + if(!gsr_string_to_int(sub, size, &capture_source->size.y)) { + gsr_log(GSR_LOG_LEVEL_ERROR, "invalid capture target value for option height: \"%.*s\", expected a number", (int)size, sub); + parse_userdata->error = GSR_ERROR_GENERIC; + return false; + } + } else if(gsr_string_starts_with(sub, size, "halign=")) { + sub += 7; + size -= 7; + if(!string_to_capture_alignment(sub, size, &capture_source->halign)) { + gsr_log(GSR_LOG_LEVEL_ERROR, "invalid capture target value for option halign: \"%.*s\", expected a \"start\", \"center\" or \"end\"", (int)size, sub); + parse_userdata->error = GSR_ERROR_GENERIC; + return false; + } + } else if(gsr_string_starts_with(sub, size, "valign=")) { + sub += 7; + size -= 7; + if(!string_to_capture_alignment(sub, size, &capture_source->valign)) { + gsr_log(GSR_LOG_LEVEL_ERROR, "invalid capture target value for option valign: \"%.*s\", expected a \"start\", \"center\" or \"end\"", (int)size, sub); + parse_userdata->error = GSR_ERROR_GENERIC; + return false; + } + } else if(gsr_string_starts_with(sub, size, "pixfmt=")) { + sub += 7; + size -= 7; + if(!string_to_v4l2_pixfmt(sub, size, &capture_source->v4l2_pixfmt)) { + gsr_log(GSR_LOG_LEVEL_ERROR, "invalid v4l2 pixfmt value for option pixfmt: \"%.*s\", expected a \"auto\", \"yuyv\" or \"mjpeg\"", (int)size, sub); + parse_userdata->error = GSR_ERROR_GENERIC; + return false; + } + } else if(gsr_string_starts_with(sub, size, "hflip=")) { + sub += 6; + size -= 6; + bool hflip = false; + if(!string_to_bool(sub, size, &hflip)) { + gsr_log(GSR_LOG_LEVEL_ERROR, "invalid bool value for option hflip: \"%.*s\", expected a \"true\" or \"false\"", (int)size, sub); + parse_userdata->error = GSR_ERROR_GENERIC; + return false; + } + + if(hflip) + capture_source->flip |= GSR_FLIP_HORIZONTAL; + } else if(gsr_string_starts_with(sub, size, "vflip=")) { + sub += 6; + size -= 6; + bool vflip = false; + if(!string_to_bool(sub, size, &vflip)) { + gsr_log(GSR_LOG_LEVEL_ERROR, "invalid bool value for option vflip: \"%.*s\", expected a \"true\" or \"false\"", (int)size, sub); + parse_userdata->error = GSR_ERROR_GENERIC; + return false; + } + + if(vflip) + capture_source->flip |= GSR_FLIP_VERTICAL; + } else if(gsr_string_starts_with(sub, size, "camera_fps=")) { + sub += 11; + size -= 11; + if(!gsr_string_to_int(sub, size, &capture_source->camera_fps)) { + gsr_log(GSR_LOG_LEVEL_ERROR, "invalid capture target value for option camera_fps: \"%.*s\", expected a number", (int)size, sub); + parse_userdata->error = GSR_ERROR_GENERIC; + return false; + } + } else if(gsr_string_starts_with(sub, size, "camera_width=")) { + sub += 13; + size -= 13; + if(!gsr_string_to_int(sub, size, &capture_source->camera_resolution.x)) { + gsr_log(GSR_LOG_LEVEL_ERROR, "invalid capture target value for option camera_width: \"%.*s\", expected a number", (int)size, sub); + parse_userdata->error = GSR_ERROR_GENERIC; + return false; + } + } else if(gsr_string_starts_with(sub, size, "camera_height=")) { + sub += 14; + size -= 14; + if(!gsr_string_to_int(sub, size, &capture_source->camera_resolution.y)) { + gsr_log(GSR_LOG_LEVEL_ERROR, "invalid capture target value for option camera_height: \"%.*s\", expected a number", (int)size, sub); + parse_userdata->error = GSR_ERROR_GENERIC; + return false; + } + } else { + gsr_log(GSR_LOG_LEVEL_ERROR, "invalid capture target option \"%.*s\", expected x, y, width, height, halign, valign, pixfmt, hflip, vflip, camera_fps, camera_width or camera_height", (int)size, sub); + parse_userdata->error = GSR_ERROR_GENERIC; + return false; + } + + return true; +} + +static int parse_capture_source_options(const char *capture_source_str, size_t capture_source_str_size, gsr_capture_source *capture_source) { + char options[1024]; + snprintf(options, sizeof(options), "%.*s", (int)capture_source_str_size, capture_source_str); + + parse_capture_source_options_userdata userdata; + userdata.capture_source = capture_source; + userdata.is_first_column = true; + userdata.error = GSR_ERROR_OK; + gsr_string_split(options, ';', parse_capture_source_options_callback, &userdata); + return userdata.error; +} + +static bool parse_capture_source_arg_callback(const char *sub, size_t size, void *userdata) { + parse_capture_source_arg_userdata *parse_userdata = userdata; + if(size == 0) + return true; + + const char *substr_start = sub; + const size_t substr_size = size; + size_t capture_source_size = size; + const char *capture_source_end = memchr(sub, ';', size); + if(capture_source_end) + capture_source_size = capture_source_end - sub; + + gsr_capture_source capture_source; + gsr_capture_source_init(&capture_source, parse_userdata->region_position, parse_userdata->region_size); + + if(gsr_string_starts_with(sub, capture_source_size, "monitor:")) { + capture_source.type = GSR_CAPTURE_SOURCE_TYPE_MONITOR; + sub += 8; + capture_source_size -= 8; + } else if(gsr_string_starts_with(sub, capture_source_size, "window:")) { + capture_source.type = GSR_CAPTURE_SOURCE_TYPE_WINDOW; + sub += 7; + capture_source_size -= 7; + } else if(gsr_string_starts_with(sub, capture_source_size, "v4l2:")) { + capture_source.type = GSR_CAPTURE_SOURCE_TYPE_V4L2; + sub += 5; + capture_source_size -= 5; + } else { + capture_source_type_from_string(sub, capture_source_size, &capture_source); + } + + snprintf(capture_source.name, sizeof(capture_source.name), "%.*s", (int)capture_source_size, sub); + + if(capture_source.type == GSR_CAPTURE_SOURCE_TYPE_WINDOW) { + if(!gsr_string_to_int64(capture_source.name, strlen(capture_source.name), &capture_source.window_id)) { + gsr_log(GSR_LOG_LEVEL_ERROR, "invalid window number %s", capture_source.name); + args_parser_print_usage(); + parse_userdata->error = GSR_ERROR_GENERIC; + return false; + } + } + + if(parse_userdata->has_multiple_capture_sources) { + capture_source.halign = GSR_CAPTURE_ALIGN_START; + capture_source.valign = GSR_CAPTURE_ALIGN_START; + capture_source.pos = (vvec2i){0, 0, VVEC2I_TYPE_PIXELS, VVEC2I_TYPE_PIXELS}; + } + + const int parse_options_result = parse_capture_source_options(substr_start, substr_size, &capture_source); + if(parse_options_result != GSR_ERROR_OK) { + parse_userdata->error = parse_options_result; + return false; + } + + gsr_capture_sources *capture_sources = parse_userdata->capture_sources; + if(!gsr_array_ensure_capacity((void**)&capture_sources->items, capture_sources->num_items, &capture_sources->capacity_items, sizeof(gsr_capture_source))) { + parse_userdata->error = GSR_ERROR_GENERIC; + return false; + } + + capture_sources->items[capture_sources->num_items] = capture_source; + ++capture_sources->num_items; + return true; +} + +int gsr_capture_sources_parse(gsr_capture_sources *self, const char *capture_source_arg, vec2i region_position, vec2i region_size) { + memset(self, 0, sizeof(*self)); + + parse_capture_source_arg_userdata userdata; + userdata.capture_sources = self; + userdata.region_position = region_position; + userdata.region_size = region_size; + userdata.has_multiple_capture_sources = strchr(capture_source_arg, '|') != NULL; + userdata.error = GSR_ERROR_OK; + + gsr_string_split(capture_source_arg, '|', parse_capture_source_arg_callback, &userdata); + if(userdata.error != GSR_ERROR_OK) + gsr_capture_sources_deinit(self); + + return userdata.error; +} + +void gsr_capture_sources_deinit(gsr_capture_sources *self) { + if(self->items) { + free(self->items); + self->items = NULL; + } + self->num_items = 0; + self->capacity_items = 0; +} + +bool gsr_capture_sources_has_type(const gsr_capture_sources *self, CaptureSourceType type) { + for(size_t i = 0; i < self->num_items; ++i) { + if(self->items[i].type == type) + return true; + } + return false; +} + +bool gsr_capture_sources_has_damage_tracked_target(const gsr_capture_sources *self) { + for(size_t i = 0; i < self->num_items; ++i) { + if(self->items[i].type != GSR_CAPTURE_SOURCE_TYPE_V4L2) + return true; + } + return false; +} + +bool gsr_capture_sources_has_region_set(const gsr_capture_sources *self) { + for(size_t i = 0; i < self->num_items; ++i) { + if(self->items[i].type == GSR_CAPTURE_SOURCE_TYPE_REGION && self->items[i].region_set) + return true; + } + return false; +} + +bool gsr_capture_sources_has_monitor_or_region(const gsr_capture_sources *self) { + for(size_t i = 0; i < self->num_items; ++i) { + if(self->items[i].type == GSR_CAPTURE_SOURCE_TYPE_MONITOR || self->items[i].type == GSR_CAPTURE_SOURCE_TYPE_REGION) + return true; + } + return false; +} -- cgit v1.2.3 From e91f1c0b75968c576fd5adb1738737cb19dd6094 Mon Sep 17 00:00:00 2001 From: dec05eba Date: Sat, 1 Aug 2026 21:38:28 +0200 Subject: Move audio input parsing and validation from main.cpp into audio_input module --- include/recorder/audio_input.h | 66 ++++++++ meson.build | 1 + src/main.cpp | 341 +++++++---------------------------------- src/recorder/audio_input.c | 336 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 455 insertions(+), 289 deletions(-) create mode 100644 include/recorder/audio_input.h create mode 100644 src/recorder/audio_input.c diff --git a/include/recorder/audio_input.h b/include/recorder/audio_input.h new file mode 100644 index 0000000..f6cb87e --- /dev/null +++ b/include/recorder/audio_input.h @@ -0,0 +1,66 @@ +#ifndef GSR_RECORDER_AUDIO_INPUT_H +#define GSR_RECORDER_AUDIO_INPUT_H + +#include +#include +#include "../sound.h" + +#define GSR_AUDIO_INPUT_NAME_MAX_SIZE 256 +#define GSR_AUDIO_TRACK_NAME_MAX_SIZE 512 + +typedef enum { + GSR_AUDIO_INPUT_TYPE_DEVICE, + GSR_AUDIO_INPUT_TYPE_APPLICATION +} gsr_audio_input_type; + +typedef struct { + char name[GSR_AUDIO_INPUT_NAME_MAX_SIZE]; + gsr_audio_input_type type; + bool inverted; +} gsr_audio_input; + +typedef struct { + char track_name[GSR_AUDIO_TRACK_NAME_MAX_SIZE]; + bool has_custom_name; + gsr_audio_input *items; + size_t num_items; + size_t capacity_items; +} gsr_merged_audio_inputs; + +typedef struct { + gsr_merged_audio_inputs *items; + size_t num_items; + size_t capacity_items; +} gsr_audio_input_tracks; + +typedef struct { + char name[GSR_AUDIO_INPUT_NAME_MAX_SIZE]; +} gsr_app_audio_name; + +typedef struct { + gsr_app_audio_name *items; + size_t num_items; + size_t capacity_items; +} gsr_app_audio_names; + +bool gsr_app_audio_names_add(gsr_app_audio_names *self, const char *name); +void gsr_app_audio_names_deinit(gsr_app_audio_names *self); + +/* Returns a |gsr_error| value. Parses one -a option value, which may contain multiple audio inputs separated by | */ +int gsr_merged_audio_inputs_parse(gsr_merged_audio_inputs *self, const char *str); +bool gsr_merged_audio_inputs_add(gsr_merged_audio_inputs *self, const gsr_audio_input *audio_input); +void gsr_merged_audio_inputs_deinit(gsr_merged_audio_inputs *self); + +/* Returns a |gsr_error| value. Parses every -a option value and validates that the audio devices exist */ +int gsr_audio_input_tracks_parse(gsr_audio_input_tracks *self, const char **audio_input_args, int num_audio_input_args, const gsr_audio_devices *audio_devices); +bool gsr_audio_input_tracks_add(gsr_audio_input_tracks *self, const gsr_merged_audio_inputs *merged_audio_inputs); +void gsr_audio_input_tracks_deinit(gsr_audio_input_tracks *self); + +bool gsr_audio_inputs_has_app_audio(const gsr_merged_audio_inputs *self); +bool gsr_audio_inputs_should_use_amix(const gsr_merged_audio_inputs *self); +bool gsr_audio_input_tracks_has_app_audio(const gsr_audio_input_tracks *self); +bool gsr_audio_input_tracks_should_use_amix(const gsr_audio_input_tracks *self); +/* Returns a |gsr_error| value. Warns about application audio names that don't match any running application */ +int gsr_audio_input_tracks_validate_app_audio(const gsr_audio_input_tracks *self, const gsr_app_audio_names *app_audio_names); + +#endif /* GSR_RECORDER_AUDIO_INPUT_H */ diff --git a/meson.build b/meson.build index af5473d..c751f9e 100644 --- a/meson.build +++ b/meson.build @@ -38,6 +38,7 @@ src = [ 'src/recorder/windowing.c', 'src/recorder/capture_source.c', 'src/recorder/capture_setup.c', + 'src/recorder/audio_input.c', 'src/egl.c', 'src/cuda.c', 'src/window_texture.c', diff --git a/src/main.cpp b/src/main.cpp index d64be78..1fb4d38 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -6,6 +6,7 @@ extern "C" { #include "../include/recorder/windowing.h" #include "../include/recorder/capture_source.h" #include "../include/recorder/capture_setup.h" +#include "../include/recorder/audio_input.h" #include "../include/recorder/error.h" #include "../include/capture/nvfbc.h" #include "../include/capture/xcomposite.h" @@ -78,23 +79,6 @@ extern "C" { #define GSR_VERSION "unknown" #endif -enum class AudioInputType { - DEVICE, - APPLICATION -}; - -struct AudioInput { - std::string name; - AudioInputType type = AudioInputType::DEVICE; - bool inverted = false; -}; - -struct MergedAudioInputs { - std::optional custom_name; - std::string track_name; - std::vector audio_inputs; -}; - // TODO: If options are not supported then they are returned (allocated) in the options. This should be free'd. // TODO: Remove LIBAVUTIL_VERSION_MAJOR checks in the future when ubuntu, pop os LTS etc update ffmpeg to >= 5.0 @@ -213,7 +197,7 @@ static void run_recording_saved_script_async(const char *script_file, const char struct AudioDeviceData { SoundDevice sound_device; - AudioInput audio_input; + gsr_audio_input audio_input; AVFilterContext *src_filter_ctx = nullptr; AVFrame *frame = nullptr; std::thread thread; // TODO: Instead of having a thread for each track, have one thread for all threads and read the data with non-blocking read @@ -541,53 +525,6 @@ static bool save_replay_async(AVCodecContext *video_codec_context, int video_str return true; } -static const gsr_audio_device* get_audio_device_by_name(const gsr_audio_devices *audio_devices, const char *name) { - for(size_t i = 0; i < audio_devices->num_items; ++i) { - if(strcmp(audio_devices->items[i].name, name) == 0) - return &audio_devices->items[i]; - } - return nullptr; -} - -static bool parse_audio_input_arg_callback(const char *sub, size_t size, void *userdata) { - MergedAudioInputs *result = (MergedAudioInputs*)userdata; - if(size == 0) - return true; - - AudioInput audio_input; - audio_input.name.assign(sub, size); - - if(gsr_string_starts_with(audio_input.name.data(), audio_input.name.size(), "name:")) { - result->custom_name = audio_input.name.substr(5); - } else if(gsr_string_starts_with(audio_input.name.data(), audio_input.name.size(), "app:")) { - audio_input.name.erase(audio_input.name.begin(), audio_input.name.begin() + 4); - audio_input.type = AudioInputType::APPLICATION; - audio_input.inverted = false; - result->audio_inputs.push_back(std::move(audio_input)); - } else if(gsr_string_starts_with(audio_input.name.data(), audio_input.name.size(), "app-inverse:")) { - audio_input.name.erase(audio_input.name.begin(), audio_input.name.begin() + 12); - audio_input.type = AudioInputType::APPLICATION; - audio_input.inverted = true; - result->audio_inputs.push_back(std::move(audio_input)); - } else if(gsr_string_starts_with(audio_input.name.data(), audio_input.name.size(), "device:")) { - audio_input.name.erase(audio_input.name.begin(), audio_input.name.begin() + 7); - audio_input.type = AudioInputType::DEVICE; - result->audio_inputs.push_back(std::move(audio_input)); - } else { - audio_input.type = AudioInputType::DEVICE; - result->audio_inputs.push_back(std::move(audio_input)); - } - - return true; -} - -static MergedAudioInputs parse_audio_input_arg(const char *str) { - MergedAudioInputs result; - result.track_name = str; - gsr_string_split(str, '|', parse_audio_input_arg_callback, &result); - return result; -} - static int init_filter_graph(AVCodecContext* audio_codec_context, AVFilterGraph** graph, AVFilterContext** sink, std::vector& src_filter_ctx, size_t num_sources) { char ch_layout[64]; int err = 0; @@ -1207,203 +1144,15 @@ static void capture_image_to_file(args_parser &arg_parser, gsr_egl *egl, gsr_win _exit(should_stop_error ? 3 : 0); } -static void match_app_audio_input_to_available_apps(const std::vector &requested_audio_inputs, const std::vector &app_audio_names) { - for(const AudioInput &request_audio_input : requested_audio_inputs) { - if(request_audio_input.type != AudioInputType::APPLICATION || request_audio_input.inverted) - continue; - - bool match = false; - for(const std::string &app_name : app_audio_names) { - if(strcasecmp(app_name.c_str(), request_audio_input.name.c_str()) == 0) { - match = true; - break; - } - } - - if(!match) { - gsr_log(GSR_LOG_LEVEL_WARNING, "no audio application with the name \"%s\" was found, expected one of the following:", request_audio_input.name.c_str()); - for(const std::string &app_name : app_audio_names) { - fprintf(stderr, " * %s\n", app_name.c_str()); - } - fprintf(stderr, " assuming this is intentional (if you are trying to record audio for applications that haven't started yet).\n"); - } - } -} - -struct AudioTrackDescription { - std::optional custom_name; - std::vector devices; - std::vector applications; - bool app_inverse = false; - - std::string to_title() const { - if(custom_name.has_value()){ - return custom_name.value(); - } - std::string title; - if(!devices.empty()) { - title += "Devices: "; - for(size_t i = 0; i < devices.size(); ++i) { - if(i > 0) - title += ", "; - title += devices[i]; - } - } - - if(!applications.empty()) { - if(!title.empty()) - title += ". "; - - if(app_inverse) - title += "All applications except: "; - else - title += "Applications: "; - - for(size_t i = 0; i < applications.size(); ++i) { - if(i > 0) - title += ", "; - title += applications[i]; - } - } - return title; - } -}; - // Manually check if the audio inputs we give exist. This is only needed for pipewire, not pulseaudio. // Pipewire instead DEFAULTS TO THE DEFAULT AUDIO INPUT. THAT'S RETARDED. // OH, YOU MISSPELLED THE AUDIO INPUT? FUCK YOU -static std::vector parse_audio_inputs(const gsr_audio_devices *audio_devices, const Arg *audio_input_arg) { - std::vector requested_audio_inputs; - - for(int i = 0; i < audio_input_arg->num_values; ++i) { - const char *audio_input = audio_input_arg->values[i]; - if(!audio_input || audio_input[0] == '\0') - continue; - - MergedAudioInputs merged_inputs = parse_audio_input_arg(audio_input); - requested_audio_inputs.push_back(merged_inputs); - AudioTrackDescription audio_track_description; - if(merged_inputs.custom_name.has_value()){ - audio_track_description.custom_name = merged_inputs.custom_name; - } - - for(AudioInput &request_audio_input : requested_audio_inputs.back().audio_inputs) { - if(request_audio_input.type == AudioInputType::APPLICATION) { - audio_track_description.applications.push_back(request_audio_input.name); - audio_track_description.app_inverse = request_audio_input.inverted; - continue; - } - - bool match = false; - - if(request_audio_input.name == "default_output") { - if(audio_devices->default_output[0] == '\0') { - gsr_log(GSR_LOG_LEVEL_ERROR, "-a default_output was specified but no default audio output is specified in the audio server"); - _exit(2); - } - match = true; - audio_track_description.devices.push_back("Default output"); - } else if(request_audio_input.name == "default_input") { - if(audio_devices->default_input[0] == '\0') { - gsr_log(GSR_LOG_LEVEL_ERROR, "-a default_input was specified but no default audio input is specified in the audio server"); - _exit(2); - } - match = true; - audio_track_description.devices.push_back("Default input"); - } else { - const gsr_audio_device *audio_device = get_audio_device_by_name(audio_devices, request_audio_input.name.c_str()); - if(audio_device) { - match = true; - audio_track_description.devices.push_back(audio_device->description); - } - } - - if(!match) { - gsr_log(GSR_LOG_LEVEL_ERROR, "Audio device '%s' is not a valid audio device, expected one of:", request_audio_input.name.c_str()); - if(audio_devices->default_output[0] != '\0') - fprintf(stderr, " default_output (Default output)\n"); - if(audio_devices->default_input[0] != '\0') - fprintf(stderr, " default_input (Default input)\n"); - for(size_t j = 0; j < audio_devices->num_items; ++j) { - fprintf(stderr, " %s (%s)\n", audio_devices->items[j].name, audio_devices->items[j].description); - } - _exit(50); - } - } - - requested_audio_inputs.back().track_name = audio_track_description.to_title(); - } - - return requested_audio_inputs; -} - -static bool audio_inputs_has_app_audio(const std::vector &audio_inputs) { - for(const auto &audio_input : audio_inputs) { - if(audio_input.type == AudioInputType::APPLICATION) - return true; - } - return false; -} - -static bool merged_audio_inputs_has_app_audio(const std::vector &merged_audio_inputs) { - for(const auto &merged_audio_input : merged_audio_inputs) { - if(audio_inputs_has_app_audio(merged_audio_input.audio_inputs)) - return true; - } - return false; -} - // Should use amix if more than 1 audio device and 0 application audio, merged -static bool audio_inputs_should_use_amix(const std::vector &audio_inputs) { - int num_audio_devices = 0; - int num_app_audio = 0; - - for(const auto &audio_input : audio_inputs) { - if(audio_input.type == AudioInputType::DEVICE) - ++num_audio_devices; - else if(audio_input.type == AudioInputType::APPLICATION) - ++num_app_audio; - } - - return num_audio_devices > 1 && num_app_audio == 0; -} - -static bool merged_audio_inputs_should_use_amix(const std::vector &merged_audio_inputs) { - for(const auto &merged_audio_input : merged_audio_inputs) { - if(audio_inputs_should_use_amix(merged_audio_input.audio_inputs)) - return true; - } - return false; -} - -static void validate_merged_audio_inputs_app_audio(const std::vector &merged_audio_inputs, const std::vector &app_audio_names) { - for(const auto &merged_audio_input : merged_audio_inputs) { - int num_app_audio = 0; - int num_app_inverted_audio = 0; - - for(const auto &audio_input : merged_audio_input.audio_inputs) { - if(audio_input.type == AudioInputType::APPLICATION) { - if(audio_input.inverted) - ++num_app_inverted_audio; - else - ++num_app_audio; - } - } - - match_app_audio_input_to_available_apps(merged_audio_input.audio_inputs, app_audio_names); - - if(num_app_audio > 0 && num_app_inverted_audio > 0) { - gsr_log(GSR_LOG_LEVEL_ERROR, "argument -a was provided with both app: and app-inverse:, only one of them can be used for one audio track"); - _exit(2); - } - } -} - /* Returns -1 if none is available */ -static std::vector create_device_audio_inputs(const std::vector &audio_inputs, AVCodecContext *audio_codec_context, int num_channels, double num_audio_frames_shift, std::vector &src_filter_ctx, bool use_amix) { +static std::vector create_device_audio_inputs(const gsr_merged_audio_inputs *merged_audio_inputs, AVCodecContext *audio_codec_context, int num_channels, double num_audio_frames_shift, std::vector &src_filter_ctx, bool use_amix) { std::vector audio_track_audio_devices; - for(size_t i = 0; i < audio_inputs.size(); ++i) { - const auto &audio_input = audio_inputs[i]; + for(size_t i = 0; i < merged_audio_inputs->num_items; ++i) { + const gsr_audio_input &audio_input = merged_audio_inputs->items[i]; AVFilterContext *src_ctx = nullptr; if(use_amix) src_ctx = src_filter_ctx[i]; @@ -1412,13 +1161,14 @@ static std::vector create_device_audio_inputs(const std::vector audio_device.audio_input = audio_input; audio_device.src_filter_ctx = src_ctx; - if(audio_input.name.empty()) { + if(audio_input.name[0] == '\0') { audio_device.sound_device.handle = NULL; audio_device.sound_device.frames = 0; } else { - const std::string description = "gsr-" + audio_input.name; - if(sound_device_get_by_name(&audio_device.sound_device, description.c_str(), audio_input.name.c_str(), description.c_str(), num_channels, audio_codec_context->frame_size, audio_codec_context_get_audio_format(audio_codec_context)) != 0) { - gsr_log(GSR_LOG_LEVEL_ERROR, "failed to get \"%s\" audio device", audio_input.name.c_str()); + char description[GSR_AUDIO_INPUT_NAME_MAX_SIZE + 8]; + snprintf(description, sizeof(description), "gsr-%s", audio_input.name); + if(sound_device_get_by_name(&audio_device.sound_device, description, audio_input.name, description, num_channels, audio_codec_context->frame_size, audio_codec_context_get_audio_format(audio_codec_context)) != 0) { + gsr_log(GSR_LOG_LEVEL_ERROR, "failed to get \"%s\" audio device", audio_input.name); _exit(1); } } @@ -1434,7 +1184,7 @@ static std::vector create_device_audio_inputs(const std::vector } #ifdef GSR_APP_AUDIO -static AudioDeviceData create_application_audio_audio_input(const MergedAudioInputs &merged_audio_inputs, AVCodecContext *audio_codec_context, int num_channels, double num_audio_frames_shift, gsr_pipewire_audio *pipewire_audio) { +static AudioDeviceData create_application_audio_audio_input(const gsr_merged_audio_inputs &merged_audio_inputs, AVCodecContext *audio_codec_context, int num_channels, double num_audio_frames_shift, gsr_pipewire_audio *pipewire_audio) { AudioDeviceData audio_device; audio_device.frame = create_audio_frame(audio_codec_context); if(!audio_device.frame) @@ -1457,16 +1207,18 @@ static AudioDeviceData create_application_audio_audio_input(const MergedAudioInp } std::vector audio_devices_sources; - for(const auto &audio_input : merged_audio_inputs.audio_inputs) { - if(audio_input.type == AudioInputType::DEVICE) - audio_devices_sources.push_back(audio_input.name.c_str()); + for(size_t i = 0; i < merged_audio_inputs.num_items; ++i) { + const gsr_audio_input &audio_input = merged_audio_inputs.items[i]; + if(audio_input.type == GSR_AUDIO_INPUT_TYPE_DEVICE) + audio_devices_sources.push_back(audio_input.name); } bool app_audio_inverted = false; std::vector app_names; - for(const auto &audio_input : merged_audio_inputs.audio_inputs) { - if(audio_input.type == AudioInputType::APPLICATION) { - app_names.push_back(audio_input.name.c_str()); + for(size_t i = 0; i < merged_audio_inputs.num_items; ++i) { + const gsr_audio_input &audio_input = merged_audio_inputs.items[i]; + if(audio_input.type == GSR_AUDIO_INPUT_TYPE_APPLICATION) { + app_names.push_back(audio_input.name); app_audio_inverted = audio_input.inverted; } } @@ -1520,15 +1272,15 @@ static void av_write_header(AVFormatContext *av_format_context, const char *ffmp av_dict_free(&options); } -static size_t calculate_estimated_replay_buffer_packets(int64_t replay_buffer_size_secs, int fps, gsr_audio_codec audio_codec, const std::vector &audio_inputs) { +static size_t calculate_estimated_replay_buffer_packets(int64_t replay_buffer_size_secs, int fps, gsr_audio_codec audio_codec, const gsr_audio_input_tracks *audio_inputs) { if(replay_buffer_size_secs == -1) return 0; int audio_fps = 0; - if(!audio_inputs.empty()) + if(audio_inputs->num_items > 0) audio_fps = GSR_AUDIO_SAMPLE_RATE / audio_codec_get_frame_size(audio_codec); - return replay_buffer_size_secs * (fps + audio_fps * audio_inputs.size()); + return replay_buffer_size_secs * (fps + audio_fps * audio_inputs->num_items); } static void set_display_server_environment_variables() { @@ -1712,11 +1464,15 @@ int main(int argc, char **argv) { if(audio_input_arg->num_values > 0) get_pulseaudio_inputs(&audio_devices); - std::vector requested_audio_inputs = parse_audio_inputs(&audio_devices, audio_input_arg); + gsr_audio_input_tracks requested_audio_inputs; + const int parse_audio_inputs_result = gsr_audio_input_tracks_parse(&requested_audio_inputs, audio_input_arg->values, audio_input_arg->num_values, &audio_devices); gsr_audio_devices_deinit(&audio_devices); + if(parse_audio_inputs_result != GSR_ERROR_OK) + _exit(gsr_error_to_exit_code(parse_audio_inputs_result)); - const bool uses_app_audio = merged_audio_inputs_has_app_audio(requested_audio_inputs); - std::vector app_audio_names; + const bool uses_app_audio = gsr_audio_input_tracks_has_app_audio(&requested_audio_inputs); + gsr_app_audio_names app_audio_names; + memset(&app_audio_names, 0, sizeof(app_audio_names)); #ifdef GSR_APP_AUDIO gsr_pipewire_audio pipewire_audio; memset(&pipewire_audio, 0, sizeof(pipewire_audio)); @@ -1732,8 +1488,8 @@ int main(int argc, char **argv) { } gsr_pipewire_audio_for_each_app(&pipewire_audio, [](const char *app_name, void *userdata) { - std::vector *app_audio_names = (std::vector*)userdata; - app_audio_names->push_back(app_name); + gsr_app_audio_names *app_audio_names = (gsr_app_audio_names*)userdata; + gsr_app_audio_names_add(app_audio_names, app_name); return true; }, &app_audio_names); } @@ -1744,7 +1500,10 @@ int main(int argc, char **argv) { } #endif - validate_merged_audio_inputs_app_audio(requested_audio_inputs, app_audio_names); + const int validate_app_audio_result = gsr_audio_input_tracks_validate_app_audio(&requested_audio_inputs, &app_audio_names); + gsr_app_audio_names_deinit(&app_audio_names); + if(validate_app_audio_result != GSR_ERROR_OK) + _exit(gsr_error_to_exit_code(validate_app_audio_result)); gsr_windowing windowing; gsr_windowing_params windowing_params; @@ -1836,7 +1595,7 @@ int main(int argc, char **argv) { const bool force_no_audio_offset = arg_parser.settings.is_livestream || arg_parser.settings.is_output_piped || (file_extension != "mp4" && file_extension != "mkv" && file_extension != "webm"); const double target_fps = 1.0 / (double)arg_parser.settings.fps; - const bool uses_amix = merged_audio_inputs_should_use_amix(requested_audio_inputs); + const bool uses_amix = gsr_audio_input_tracks_should_use_amix(&requested_audio_inputs); arg_parser.settings.audio_codec = select_audio_codec_with_fallback(arg_parser.settings.audio_codec, file_extension.c_str(), uses_amix); vec2i video_size = {0, 0}; @@ -1849,11 +1608,14 @@ int main(int argc, char **argv) { // (Some?) livestreaming services require at least one audio track to work. // If not audio is provided then create one silent audio track. - if(arg_parser.settings.is_livestream && requested_audio_inputs.empty()) { + if(arg_parser.settings.is_livestream && requested_audio_inputs.num_items == 0) { gsr_log(GSR_LOG_LEVEL_INFO, "live streaming but no audio track was added. Adding a silent audio track"); - MergedAudioInputs mai; - mai.audio_inputs.push_back({""}); - requested_audio_inputs.push_back(std::move(mai)); + gsr_merged_audio_inputs silent_audio_track; + memset(&silent_audio_track, 0, sizeof(silent_audio_track)); + gsr_audio_input silent_audio_input; + memset(&silent_audio_input, 0, sizeof(silent_audio_input)); + if(!gsr_merged_audio_inputs_add(&silent_audio_track, &silent_audio_input) || !gsr_audio_input_tracks_add(&requested_audio_inputs, &silent_audio_track)) + _exit(1); } AVStream *video_stream = nullptr; @@ -1889,7 +1651,7 @@ int main(int argc, char **argv) { video_frame->colorspace = video_codec_context->colorspace; video_frame->chroma_location = video_codec_context->chroma_sample_location; - const size_t estimated_replay_buffer_packets = calculate_estimated_replay_buffer_packets(arg_parser.settings.replay_buffer_size_secs, arg_parser.settings.fps, arg_parser.settings.audio_codec, requested_audio_inputs); + const size_t estimated_replay_buffer_packets = calculate_estimated_replay_buffer_packets(arg_parser.settings.replay_buffer_size_secs, arg_parser.settings.fps, arg_parser.settings.audio_codec, &requested_audio_inputs); gsr_encoder encoder; if(!gsr_encoder_init(&encoder, arg_parser.settings.replay_storage, estimated_replay_buffer_packets, arg_parser.settings.replay_buffer_size_secs, arg_parser.settings.filename)) { gsr_log(GSR_LOG_LEVEL_ERROR, "failed to create encoder"); @@ -1956,8 +1718,9 @@ int main(int argc, char **argv) { int audio_max_frame_size = 1024; int audio_stream_index = VIDEO_STREAM_INDEX + 1; - for(const MergedAudioInputs &merged_audio_inputs : requested_audio_inputs) { - const bool use_amix = audio_inputs_should_use_amix(merged_audio_inputs.audio_inputs); + for(size_t audio_track_index = 0; audio_track_index < requested_audio_inputs.num_items; ++audio_track_index) { + const gsr_merged_audio_inputs &merged_audio_inputs = requested_audio_inputs.items[audio_track_index]; + const bool use_amix = gsr_audio_inputs_should_use_amix(&merged_audio_inputs); AVCodecContext *audio_codec_context = create_audio_codec_context(arg_parser.settings.fps, arg_parser.settings.audio_codec, use_amix, arg_parser.settings.audio_bitrate); if(!audio_codec_context) _exit(1); @@ -1969,8 +1732,8 @@ int main(int argc, char **argv) { gsr_log(GSR_LOG_LEVEL_ERROR, "added too many audio sources"); } - if(audio_stream && !merged_audio_inputs.track_name.empty() && !arg_parser.settings.exclude_metadata) - av_dict_set(&audio_stream->metadata, "title", merged_audio_inputs.track_name.c_str(), 0); + if(audio_stream && !merged_audio_inputs.track_name[0] == '\0' && !arg_parser.settings.exclude_metadata) + av_dict_set(&audio_stream->metadata, "title", merged_audio_inputs.track_name, 0); if(!open_audio(audio_codec_context, arg_parser.settings.ffmpeg_audio_opts)) _exit(1); @@ -1989,7 +1752,7 @@ int main(int argc, char **argv) { AVFilterGraph *graph = nullptr; AVFilterContext *sink = nullptr; if(use_amix) { - int err = init_filter_graph(audio_codec_context, &graph, &sink, src_filter_ctx, merged_audio_inputs.audio_inputs.size()); + int err = init_filter_graph(audio_codec_context, &graph, &sink, src_filter_ctx, merged_audio_inputs.num_items); if(err < 0) { gsr_log(GSR_LOG_LEVEL_ERROR, "failed to create audio filter"); _exit(1); @@ -2005,13 +1768,13 @@ int main(int argc, char **argv) { const double num_audio_frames_shift = audio_startup_time_seconds / timeout_sec; std::vector audio_track_audio_devices; - if(audio_inputs_has_app_audio(merged_audio_inputs.audio_inputs)) { + if(gsr_audio_inputs_has_app_audio(&merged_audio_inputs)) { assert(!use_amix); #ifdef GSR_APP_AUDIO audio_track_audio_devices.push_back(create_application_audio_audio_input(merged_audio_inputs, audio_codec_context, num_channels, num_audio_frames_shift, &pipewire_audio)); #endif } else { - audio_track_audio_devices = create_device_audio_inputs(merged_audio_inputs.audio_inputs, audio_codec_context, num_channels, num_audio_frames_shift, src_filter_ctx, use_amix); + audio_track_audio_devices = create_device_audio_inputs(&merged_audio_inputs, audio_codec_context, num_channels, num_audio_frames_shift, src_filter_ctx, use_amix); } AudioTrack audio_track; diff --git a/src/recorder/audio_input.c b/src/recorder/audio_input.c new file mode 100644 index 0000000..bbfb9bd --- /dev/null +++ b/src/recorder/audio_input.c @@ -0,0 +1,336 @@ +#include "../../include/recorder/audio_input.h" +#include "../../include/recorder/error.h" +#include "../../include/utils.h" +#include "../../include/log.h" + +#include +#include +#include +#include + +typedef struct { + gsr_merged_audio_inputs *merged_audio_inputs; + int error; +} parse_audio_input_userdata; + +bool gsr_app_audio_names_add(gsr_app_audio_names *self, const char *name) { + if(!gsr_array_ensure_capacity((void**)&self->items, self->num_items, &self->capacity_items, sizeof(gsr_app_audio_name))) + return false; + + snprintf(self->items[self->num_items].name, sizeof(self->items[self->num_items].name), "%s", name); + ++self->num_items; + return true; +} + +void gsr_app_audio_names_deinit(gsr_app_audio_names *self) { + if(self->items) { + free(self->items); + self->items = NULL; + } + self->num_items = 0; + self->capacity_items = 0; +} + +bool gsr_merged_audio_inputs_add(gsr_merged_audio_inputs *self, const gsr_audio_input *audio_input) { + if(!gsr_array_ensure_capacity((void**)&self->items, self->num_items, &self->capacity_items, sizeof(gsr_audio_input))) + return false; + + self->items[self->num_items] = *audio_input; + ++self->num_items; + return true; +} + +void gsr_merged_audio_inputs_deinit(gsr_merged_audio_inputs *self) { + if(self->items) { + free(self->items); + self->items = NULL; + } + self->num_items = 0; + self->capacity_items = 0; +} + +static bool parse_audio_input_callback(const char *sub, size_t size, void *userdata) { + parse_audio_input_userdata *parse_userdata = userdata; + if(size == 0) + return true; + + gsr_audio_input audio_input; + memset(&audio_input, 0, sizeof(audio_input)); + snprintf(audio_input.name, sizeof(audio_input.name), "%.*s", (int)size, sub); + + const size_t name_size = strlen(audio_input.name); + if(gsr_string_starts_with(audio_input.name, name_size, "name:")) { + snprintf(parse_userdata->merged_audio_inputs->track_name, sizeof(parse_userdata->merged_audio_inputs->track_name), "%s", audio_input.name + 5); + parse_userdata->merged_audio_inputs->has_custom_name = true; + return true; + } else if(gsr_string_starts_with(audio_input.name, name_size, "app:")) { + memmove(audio_input.name, audio_input.name + 4, name_size - 4 + 1); + audio_input.type = GSR_AUDIO_INPUT_TYPE_APPLICATION; + audio_input.inverted = false; + } else if(gsr_string_starts_with(audio_input.name, name_size, "app-inverse:")) { + memmove(audio_input.name, audio_input.name + 12, name_size - 12 + 1); + audio_input.type = GSR_AUDIO_INPUT_TYPE_APPLICATION; + audio_input.inverted = true; + } else if(gsr_string_starts_with(audio_input.name, name_size, "device:")) { + memmove(audio_input.name, audio_input.name + 7, name_size - 7 + 1); + audio_input.type = GSR_AUDIO_INPUT_TYPE_DEVICE; + } else { + audio_input.type = GSR_AUDIO_INPUT_TYPE_DEVICE; + } + + if(!gsr_merged_audio_inputs_add(parse_userdata->merged_audio_inputs, &audio_input)) { + parse_userdata->error = GSR_ERROR_GENERIC; + return false; + } + + return true; +} + +int gsr_merged_audio_inputs_parse(gsr_merged_audio_inputs *self, const char *str) { + memset(self, 0, sizeof(*self)); + + parse_audio_input_userdata userdata; + userdata.merged_audio_inputs = self; + userdata.error = GSR_ERROR_OK; + + gsr_string_split(str, '|', parse_audio_input_callback, &userdata); + if(userdata.error != GSR_ERROR_OK) { + gsr_merged_audio_inputs_deinit(self); + return userdata.error; + } + + return GSR_ERROR_OK; +} + +bool gsr_audio_input_tracks_add(gsr_audio_input_tracks *self, const gsr_merged_audio_inputs *merged_audio_inputs) { + if(!gsr_array_ensure_capacity((void**)&self->items, self->num_items, &self->capacity_items, sizeof(gsr_merged_audio_inputs))) + return false; + + self->items[self->num_items] = *merged_audio_inputs; + ++self->num_items; + return true; +} + +void gsr_audio_input_tracks_deinit(gsr_audio_input_tracks *self) { + for(size_t i = 0; i < self->num_items; ++i) { + gsr_merged_audio_inputs_deinit(&self->items[i]); + } + + if(self->items) { + free(self->items); + self->items = NULL; + } + self->num_items = 0; + self->capacity_items = 0; +} + +static const gsr_audio_device* get_audio_device_by_name(const gsr_audio_devices *audio_devices, const char *name) { + for(size_t i = 0; i < audio_devices->num_items; ++i) { + if(strcmp(audio_devices->items[i].name, name) == 0) + return &audio_devices->items[i]; + } + return NULL; +} + +static void audio_track_title_append(char *title, size_t title_size, size_t *offset, const char *str) { + const int written = snprintf(title + *offset, *offset < title_size ? title_size - *offset : 0, "%s", str); + if(written > 0) + *offset += written; +} + +/* Manually check if the audio inputs we give exist. This is only needed for pipewire, not pulseaudio. + Pipewire instead defaults to the default audio input if the audio input doesn't exist */ +static int validate_audio_inputs_get_track_name(const gsr_merged_audio_inputs *merged_audio_inputs, const gsr_audio_devices *audio_devices, char *track_name, size_t track_name_size) { + size_t offset = 0; + bool has_devices = false; + bool has_applications = false; + bool app_inverse = false; + track_name[0] = '\0'; + + for(size_t i = 0; i < merged_audio_inputs->num_items; ++i) { + const gsr_audio_input *audio_input = &merged_audio_inputs->items[i]; + if(audio_input->type == GSR_AUDIO_INPUT_TYPE_APPLICATION) + continue; + + const char *device_description = NULL; + if(strcmp(audio_input->name, "default_output") == 0) { + if(audio_devices->default_output[0] == '\0') { + gsr_log(GSR_LOG_LEVEL_ERROR, "-a default_output was specified but no default audio output is specified in the audio server"); + return GSR_ERROR_UNSUPPORTED; + } + device_description = "Default output"; + } else if(strcmp(audio_input->name, "default_input") == 0) { + if(audio_devices->default_input[0] == '\0') { + gsr_log(GSR_LOG_LEVEL_ERROR, "-a default_input was specified but no default audio input is specified in the audio server"); + return GSR_ERROR_UNSUPPORTED; + } + device_description = "Default input"; + } else { + const gsr_audio_device *audio_device = get_audio_device_by_name(audio_devices, audio_input->name); + if(audio_device) + device_description = audio_device->description; + } + + if(!device_description) { + gsr_log(GSR_LOG_LEVEL_ERROR, "Audio device '%s' is not a valid audio device, expected one of:", audio_input->name); + if(audio_devices->default_output[0] != '\0') + fprintf(stderr, " default_output (Default output)\n"); + if(audio_devices->default_input[0] != '\0') + fprintf(stderr, " default_input (Default input)\n"); + for(size_t j = 0; j < audio_devices->num_items; ++j) { + fprintf(stderr, " %s (%s)\n", audio_devices->items[j].name, audio_devices->items[j].description); + } + return GSR_ERROR_AUDIO_DEVICE_NOT_FOUND; + } + + audio_track_title_append(track_name, track_name_size, &offset, has_devices ? ", " : "Devices: "); + audio_track_title_append(track_name, track_name_size, &offset, device_description); + has_devices = true; + } + + for(size_t i = 0; i < merged_audio_inputs->num_items; ++i) { + const gsr_audio_input *audio_input = &merged_audio_inputs->items[i]; + if(audio_input->type != GSR_AUDIO_INPUT_TYPE_APPLICATION) + continue; + + app_inverse = audio_input->inverted; + if(!has_applications) { + if(has_devices) + audio_track_title_append(track_name, track_name_size, &offset, ". "); + audio_track_title_append(track_name, track_name_size, &offset, app_inverse ? "All applications except: " : "Applications: "); + } else { + audio_track_title_append(track_name, track_name_size, &offset, ", "); + } + + audio_track_title_append(track_name, track_name_size, &offset, audio_input->name); + has_applications = true; + } + + return GSR_ERROR_OK; +} + +int gsr_audio_input_tracks_parse(gsr_audio_input_tracks *self, const char **audio_input_args, int num_audio_input_args, const gsr_audio_devices *audio_devices) { + memset(self, 0, sizeof(*self)); + + for(int i = 0; i < num_audio_input_args; ++i) { + const char *audio_input = audio_input_args[i]; + if(!audio_input || audio_input[0] == '\0') + continue; + + gsr_merged_audio_inputs merged_audio_inputs; + const int parse_result = gsr_merged_audio_inputs_parse(&merged_audio_inputs, audio_input); + if(parse_result != GSR_ERROR_OK) { + gsr_audio_input_tracks_deinit(self); + return parse_result; + } + + char track_name[GSR_AUDIO_TRACK_NAME_MAX_SIZE]; + const int validate_result = validate_audio_inputs_get_track_name(&merged_audio_inputs, audio_devices, track_name, sizeof(track_name)); + if(validate_result != GSR_ERROR_OK) { + gsr_merged_audio_inputs_deinit(&merged_audio_inputs); + gsr_audio_input_tracks_deinit(self); + return validate_result; + } + + if(!merged_audio_inputs.has_custom_name) + snprintf(merged_audio_inputs.track_name, sizeof(merged_audio_inputs.track_name), "%s", track_name); + + if(!gsr_audio_input_tracks_add(self, &merged_audio_inputs)) { + gsr_merged_audio_inputs_deinit(&merged_audio_inputs); + gsr_audio_input_tracks_deinit(self); + return GSR_ERROR_GENERIC; + } + } + + return GSR_ERROR_OK; +} + +bool gsr_audio_inputs_has_app_audio(const gsr_merged_audio_inputs *self) { + for(size_t i = 0; i < self->num_items; ++i) { + if(self->items[i].type == GSR_AUDIO_INPUT_TYPE_APPLICATION) + return true; + } + return false; +} + +/* Should use amix if more than 1 audio device and 0 application audio, merged */ +bool gsr_audio_inputs_should_use_amix(const gsr_merged_audio_inputs *self) { + int num_audio_devices = 0; + int num_app_audio = 0; + + for(size_t i = 0; i < self->num_items; ++i) { + if(self->items[i].type == GSR_AUDIO_INPUT_TYPE_DEVICE) + ++num_audio_devices; + else if(self->items[i].type == GSR_AUDIO_INPUT_TYPE_APPLICATION) + ++num_app_audio; + } + + return num_audio_devices > 1 && num_app_audio == 0; +} + +bool gsr_audio_input_tracks_has_app_audio(const gsr_audio_input_tracks *self) { + for(size_t i = 0; i < self->num_items; ++i) { + if(gsr_audio_inputs_has_app_audio(&self->items[i])) + return true; + } + return false; +} + +bool gsr_audio_input_tracks_should_use_amix(const gsr_audio_input_tracks *self) { + for(size_t i = 0; i < self->num_items; ++i) { + if(gsr_audio_inputs_should_use_amix(&self->items[i])) + return true; + } + return false; +} + +static void match_app_audio_input_to_available_apps(const gsr_merged_audio_inputs *merged_audio_inputs, const gsr_app_audio_names *app_audio_names) { + for(size_t i = 0; i < merged_audio_inputs->num_items; ++i) { + const gsr_audio_input *audio_input = &merged_audio_inputs->items[i]; + if(audio_input->type != GSR_AUDIO_INPUT_TYPE_APPLICATION || audio_input->inverted) + continue; + + bool match = false; + for(size_t j = 0; j < app_audio_names->num_items; ++j) { + if(strcasecmp(app_audio_names->items[j].name, audio_input->name) == 0) { + match = true; + break; + } + } + + if(!match) { + gsr_log(GSR_LOG_LEVEL_WARNING, "no audio application with the name \"%s\" was found, expected one of the following:", audio_input->name); + for(size_t j = 0; j < app_audio_names->num_items; ++j) { + fprintf(stderr, " * %s\n", app_audio_names->items[j].name); + } + fprintf(stderr, " assuming this is intentional (if you are trying to record audio for applications that haven't started yet).\n"); + } + } +} + +int gsr_audio_input_tracks_validate_app_audio(const gsr_audio_input_tracks *self, const gsr_app_audio_names *app_audio_names) { + for(size_t i = 0; i < self->num_items; ++i) { + const gsr_merged_audio_inputs *merged_audio_inputs = &self->items[i]; + int num_app_audio = 0; + int num_app_inverted_audio = 0; + + for(size_t j = 0; j < merged_audio_inputs->num_items; ++j) { + const gsr_audio_input *audio_input = &merged_audio_inputs->items[j]; + if(audio_input->type == GSR_AUDIO_INPUT_TYPE_APPLICATION) { + if(audio_input->inverted) + ++num_app_inverted_audio; + else + ++num_app_audio; + } + } + + match_app_audio_input_to_available_apps(merged_audio_inputs, app_audio_names); + + if(num_app_audio > 0 && num_app_inverted_audio > 0) { + gsr_log(GSR_LOG_LEVEL_ERROR, "argument -a was provided with both app: and app-inverse:, only one of them can be used for one audio track"); + return GSR_ERROR_UNSUPPORTED; + } + } + + return GSR_ERROR_OK; +} -- cgit v1.2.3 From bd12a0ede46bbd92510f1cd82b04383e4a9e83c8 Mon Sep 17 00:00:00 2001 From: dec05eba Date: Sat, 1 Aug 2026 21:49:08 +0200 Subject: Move audio capture from main.cpp into audio_capture module and replace std::thread with pthread Audio device and mix threads are now pthreads with explicit context structs, and the paused time tracking moved into a gsr_recording_clock. --- include/recorder/audio_capture.h | 92 ++++++ include/recorder/recording_clock.h | 29 ++ meson.build | 2 + src/main.cpp | 582 ++++------------------------------- src/recorder/audio_capture.c | 610 +++++++++++++++++++++++++++++++++++++ src/recorder/recording_clock.c | 61 ++++ 6 files changed, 857 insertions(+), 519 deletions(-) create mode 100644 include/recorder/audio_capture.h create mode 100644 include/recorder/recording_clock.h create mode 100644 src/recorder/audio_capture.c create mode 100644 src/recorder/recording_clock.c diff --git a/include/recorder/audio_capture.h b/include/recorder/audio_capture.h new file mode 100644 index 0000000..68390aa --- /dev/null +++ b/include/recorder/audio_capture.h @@ -0,0 +1,92 @@ +#ifndef GSR_RECORDER_AUDIO_CAPTURE_H +#define GSR_RECORDER_AUDIO_CAPTURE_H + +#include +#include +#include +#include +#include "../sound.h" +#include "recording_clock.h" +#include "../encoder/encoder.h" +#include "audio_input.h" + +#ifdef GSR_APP_AUDIO +#include "../pipewire_audio.h" +#endif + +#include +#include + +#define GSR_MAX_AUDIO_SOURCES_PER_TRACK 32 + +typedef struct gsr_audio_capture gsr_audio_capture; +typedef struct gsr_audio_track gsr_audio_track; +typedef struct gsr_audio_device_capture gsr_audio_device_capture; + +typedef struct { + gsr_audio_capture *audio_capture; + gsr_audio_track *track; + gsr_audio_device_capture *device; +} gsr_audio_device_thread_userdata; + +struct gsr_audio_device_capture { + SoundDevice sound_device; + gsr_audio_input audio_input; + AVFilterContext *src_filter_ctx; + AVFrame *frame; + pthread_t thread; + bool thread_created; + gsr_audio_device_thread_userdata thread_userdata; +}; + +/* TODO: Instead of having a thread for each audio device, have one thread for all of them and read the data with non-blocking read */ +struct gsr_audio_track { + char name[GSR_AUDIO_TRACK_NAME_MAX_SIZE]; + AVCodecContext *codec_context; + gsr_audio_device_capture *audio_devices; + size_t num_audio_devices; + AVFilterGraph *graph; + AVFilterContext *sink; + int stream_index; + int64_t pts; +}; + +struct gsr_audio_capture { + gsr_audio_track *tracks; + size_t num_tracks; + size_t capacity_tracks; + + pthread_mutex_t filter_mutex; + bool filter_mutex_initialized; + pthread_t amix_thread; + bool amix_thread_created; + uint8_t *empty_audio; + + gsr_encoder *encoder; + gsr_recording_clock *clock; + const volatile sig_atomic_t *running; +}; + +/* Returns a |gsr_error| value */ +int gsr_audio_capture_init(gsr_audio_capture *self, gsr_encoder *encoder, gsr_recording_clock *clock, const volatile sig_atomic_t *running); +void gsr_audio_capture_deinit(gsr_audio_capture *self); + +bool gsr_audio_capture_add_track(gsr_audio_capture *self, const gsr_audio_track *track); +/* Allocates the silence buffer and starts one thread per audio device, plus the amix thread when |uses_amix| is set */ +int gsr_audio_capture_start(gsr_audio_capture *self, int audio_max_frame_size, bool uses_amix); +void gsr_audio_capture_join_threads(gsr_audio_capture *self); +void gsr_audio_capture_lock_filter(gsr_audio_capture *self); +void gsr_audio_capture_unlock_filter(gsr_audio_capture *self); + +/* Returns 0 on success, or a negative value on failure. |src_filter_ctx| must have room for |num_sources| items */ +int gsr_audio_init_filter_graph(AVCodecContext *audio_codec_context, AVFilterGraph **graph, AVFilterContext **sink, AVFilterContext **src_filter_ctx, size_t num_sources); + +/* Returns a |gsr_error| value. Opens the sound devices of one audio track */ +int gsr_audio_track_init_device_inputs(gsr_audio_track *self, const gsr_merged_audio_inputs *merged_audio_inputs, AVCodecContext *audio_codec_context, int num_channels, double num_audio_frames_shift, AVFilterContext **src_filter_ctx, bool use_amix); +#ifdef GSR_APP_AUDIO +/* Returns a |gsr_error| value. Creates a combined sink that application audio is routed to */ +int gsr_audio_track_init_application_input(gsr_audio_track *self, const gsr_merged_audio_inputs *merged_audio_inputs, AVCodecContext *audio_codec_context, int num_channels, double num_audio_frames_shift, gsr_pipewire_audio *pipewire_audio); +#endif +void gsr_audio_track_deinit(gsr_audio_track *self); + +#endif /* GSR_RECORDER_AUDIO_CAPTURE_H */ diff --git a/include/recorder/recording_clock.h b/include/recorder/recording_clock.h new file mode 100644 index 0000000..4e302b6 --- /dev/null +++ b/include/recorder/recording_clock.h @@ -0,0 +1,29 @@ +#ifndef GSR_RECORDER_RECORDING_CLOCK_H +#define GSR_RECORDER_RECORDING_CLOCK_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* Monotonic clock that excludes the time the recording has been paused. Safe to use from multiple threads */ +typedef struct gsr_recording_clock gsr_recording_clock; + +gsr_recording_clock* gsr_recording_clock_create(void); +void gsr_recording_clock_destroy(gsr_recording_clock *self); + +/* Sets the time the recording started to now */ +void gsr_recording_clock_start(gsr_recording_clock *self); +double gsr_recording_clock_get_start_time(const gsr_recording_clock *self); +/* Returns the current time, excluding the time the recording has been paused */ +double gsr_recording_clock_get_time(const gsr_recording_clock *self); + +void gsr_recording_clock_set_paused(gsr_recording_clock *self, bool paused); +bool gsr_recording_clock_is_paused(const gsr_recording_clock *self); + +#ifdef __cplusplus +} +#endif + +#endif /* GSR_RECORDER_RECORDING_CLOCK_H */ diff --git a/meson.build b/meson.build index c751f9e..d56b21c 100644 --- a/meson.build +++ b/meson.build @@ -39,6 +39,8 @@ src = [ 'src/recorder/capture_source.c', 'src/recorder/capture_setup.c', 'src/recorder/audio_input.c', + 'src/recorder/audio_capture.c', + 'src/recorder/recording_clock.c', 'src/egl.c', 'src/cuda.c', 'src/window_texture.c', diff --git a/src/main.cpp b/src/main.cpp index 1fb4d38..b3056c3 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -7,6 +7,7 @@ extern "C" { #include "../include/recorder/capture_source.h" #include "../include/recorder/capture_setup.h" #include "../include/recorder/audio_input.h" +#include "../include/recorder/audio_capture.h" #include "../include/recorder/error.h" #include "../include/capture/nvfbc.h" #include "../include/capture/xcomposite.h" @@ -195,26 +196,7 @@ static void run_recording_saved_script_async(const char *script_file, const char } } -struct AudioDeviceData { - SoundDevice sound_device; - gsr_audio_input audio_input; - AVFilterContext *src_filter_ctx = nullptr; - AVFrame *frame = nullptr; - std::thread thread; // TODO: Instead of having a thread for each track, have one thread for all threads and read the data with non-blocking read -}; - // TODO: Cleanup -struct AudioTrack { - std::string name; - AVCodecContext *codec_context = nullptr; - - std::vector audio_devices; - AVFilterGraph *graph = nullptr; - AVFilterContext *sink = nullptr; - int stream_index = 0; - int64_t pts = 0; -}; - static bool add_hdr_metadata_to_video_stream(gsr_capture *cap, AVStream *video_stream) { size_t light_metadata_size = 0; size_t mastering_display_metadata_size = 0; @@ -286,7 +268,7 @@ static void set_format_context_options(AVFormatContext *av_format_context) { } struct RecordingStartAudio { - const AudioTrack *audio_track; + const gsr_audio_track *audio_track; AVStream *stream; }; @@ -296,7 +278,7 @@ struct RecordingStartResult { std::vector audio_inputs; }; -static RecordingStartResult start_recording_create_streams(const char *filename, const args_parser &arg_parser, AVCodecContext *video_codec_context, const std::vector &audio_tracks, bool hdr, gsr_video_sources *video_sources) { +static RecordingStartResult start_recording_create_streams(const char *filename, const args_parser &arg_parser, AVCodecContext *video_codec_context, const gsr_audio_capture *audio_capture, bool hdr, gsr_video_sources *video_sources) { AVFormatContext *av_format_context; avformat_alloc_output_context2(&av_format_context, nullptr, arg_parser.settings.container_format, filename); set_format_context_options(av_format_context); @@ -305,12 +287,13 @@ static RecordingStartResult start_recording_create_streams(const char *filename, avcodec_parameters_from_context(video_stream->codecpar, video_codec_context); RecordingStartResult result; - result.audio_inputs.reserve(audio_tracks.size()); + result.audio_inputs.reserve(audio_capture->num_tracks); - for(const AudioTrack &audio_track : audio_tracks) { + for(size_t audio_track_index = 0; audio_track_index < audio_capture->num_tracks; ++audio_track_index) { + const gsr_audio_track &audio_track = audio_capture->tracks[audio_track_index]; AVStream *audio_stream = create_stream(av_format_context, audio_track.codec_context); - if(!audio_track.name.empty() && !arg_parser.settings.exclude_metadata) - av_dict_set(&audio_stream->metadata, "title", audio_track.name.c_str(), 0); + if(!audio_track.name[0] == '\0' && !arg_parser.settings.exclude_metadata) + av_dict_set(&audio_stream->metadata, "title", audio_track.name, 0); avcodec_parameters_from_context(audio_stream->codecpar, audio_track.codec_context); result.audio_inputs.push_back({&audio_track, audio_stream}); } @@ -394,7 +377,7 @@ struct AudioPtsOffset { int stream_index = 0; }; -static bool save_replay_async(AVCodecContext *video_codec_context, int video_stream_index, const std::vector &audio_tracks, gsr_encoder *encoder, const args_parser &arg_parser, const std::string &file_extension, bool date_folders, bool hdr, gsr_video_sources *video_sources, int current_save_replay_seconds) { +static bool save_replay_async(AVCodecContext *video_codec_context, int video_stream_index, const gsr_audio_capture *audio_capture, gsr_encoder *encoder, const args_parser &arg_parser, const std::string &file_extension, bool date_folders, bool hdr, gsr_video_sources *video_sources, int current_save_replay_seconds) { if(save_replay_thread.valid()) return true; @@ -420,15 +403,16 @@ static bool save_replay_async(AVCodecContext *video_codec_context, int video_str const int64_t video_pts_offset = gsr_replay_buffer_iterator_get_packet(cloned_replay_buffer, video_start_iterator)->pts; std::vector audio_pts_offsets; - audio_pts_offsets.reserve(audio_tracks.size()); - for(const AudioTrack &audio_track : audio_tracks) { + audio_pts_offsets.reserve(audio_capture->num_tracks); + for(size_t audio_track_index = 0; audio_track_index < audio_capture->num_tracks; ++audio_track_index) { + const gsr_audio_track &audio_track = audio_capture->tracks[audio_track_index]; const gsr_replay_buffer_iterator audio_start_iterator = gsr_replay_buffer_find_keyframe(cloned_replay_buffer, video_start_iterator, audio_track.stream_index, false); const int64_t audio_pts_offset = audio_start_iterator.packet_index == (size_t)-1 ? 0 : gsr_replay_buffer_iterator_get_packet(cloned_replay_buffer, audio_start_iterator)->pts; audio_pts_offsets.push_back(AudioPtsOffset{audio_pts_offset, audio_track.stream_index}); } std::string output_filepath = create_new_recording_filepath_from_timestamp(arg_parser.settings.filename, "Replay", file_extension, date_folders); - RecordingStartResult recording_start_result = start_recording_create_streams(output_filepath.c_str(), arg_parser, video_codec_context, audio_tracks, hdr, video_sources); + RecordingStartResult recording_start_result = start_recording_create_streams(output_filepath.c_str(), arg_parser, video_codec_context, audio_capture, hdr, video_sources); if(!recording_start_result.av_format_context) { pthread_mutex_lock(&encoder->replay_mutex); gsr_replay_buffer_destroy(cloned_replay_buffer); @@ -489,7 +473,7 @@ static bool save_replay_async(AVCodecContext *video_codec_context, int video_str continue; } - const AudioTrack *audio_track = recording_start_audio->audio_track; + const gsr_audio_track *audio_track = recording_start_audio->audio_track; stream = recording_start_audio->stream; codec_context = audio_track->codec_context; @@ -525,140 +509,6 @@ static bool save_replay_async(AVCodecContext *video_codec_context, int video_str return true; } -static int init_filter_graph(AVCodecContext* audio_codec_context, AVFilterGraph** graph, AVFilterContext** sink, std::vector& src_filter_ctx, size_t num_sources) { - char ch_layout[64]; - int err = 0; - ch_layout[0] = '\0'; - - // C89-style variable declaration to - // avoid problems because of goto - AVFilterGraph* filter_graph = nullptr; - AVFilterContext* mix_ctx = nullptr; - - const AVFilter* mix_filter = nullptr; - const AVFilter* abuffersink = nullptr; - AVFilterContext* abuffersink_ctx = nullptr; - char args[512] = { 0 }; -#if LIBAVFILTER_VERSION_INT >= AV_VERSION_INT(7, 107, 100) - bool normalize = false; -#endif - - filter_graph = avfilter_graph_alloc(); - if (!filter_graph) { - fprintf(stderr, "Unable to create filter graph.\n"); - err = AVERROR(ENOMEM); - goto fail; - } - - for(size_t i = 0; i < num_sources; ++i) { - const AVFilter *abuffer = avfilter_get_by_name("abuffer"); - if (!abuffer) { - fprintf(stderr, "Could not find the abuffer filter.\n"); - err = AVERROR_FILTER_NOT_FOUND; - goto fail; - } - - AVFilterContext *abuffer_ctx = avfilter_graph_alloc_filter(filter_graph, abuffer, NULL); - if (!abuffer_ctx) { - fprintf(stderr, "Could not allocate the abuffer instance.\n"); - err = AVERROR(ENOMEM); - goto fail; - } - - #if LIBAVCODEC_VERSION_MAJOR < 60 - av_get_channel_layout_string(ch_layout, sizeof(ch_layout), 0, AV_CH_LAYOUT_STEREO); - #else - av_channel_layout_describe(&audio_codec_context->ch_layout, ch_layout, sizeof(ch_layout)); - #endif - av_opt_set (abuffer_ctx, "channel_layout", ch_layout, AV_OPT_SEARCH_CHILDREN); - av_opt_set (abuffer_ctx, "sample_fmt", av_get_sample_fmt_name(audio_codec_context->sample_fmt), AV_OPT_SEARCH_CHILDREN); - av_opt_set_q (abuffer_ctx, "time_base", audio_codec_context->time_base, AV_OPT_SEARCH_CHILDREN); - av_opt_set_int(abuffer_ctx, "sample_rate", audio_codec_context->sample_rate, AV_OPT_SEARCH_CHILDREN); - av_opt_set_int(abuffer_ctx, "bit_rate", audio_codec_context->bit_rate, AV_OPT_SEARCH_CHILDREN); - - err = avfilter_init_str(abuffer_ctx, NULL); - if (err < 0) { - fprintf(stderr, "Could not initialize the abuffer filter.\n"); - goto fail; - } - - src_filter_ctx.push_back(abuffer_ctx); - } - - mix_filter = avfilter_get_by_name("amix"); - if (!mix_filter) { - av_log(NULL, AV_LOG_ERROR, "Could not find the mix filter.\n"); - err = AVERROR_FILTER_NOT_FOUND; - goto fail; - } - -#if LIBAVFILTER_VERSION_INT >= AV_VERSION_INT(7, 107, 100) - snprintf(args, sizeof(args), "inputs=%d:normalize=%s", (int)num_sources, normalize ? "true" : "false"); -#else - snprintf(args, sizeof(args), "inputs=%d", (int)num_sources); - gsr_log(GSR_LOG_LEVEL_WARNING, "your ffmpeg version doesn't support disabling normalizing of mixed audio. Volume might be lower than expected"); -#endif - - err = avfilter_graph_create_filter(&mix_ctx, mix_filter, "amix", args, NULL, filter_graph); - if (err < 0) { - av_log(NULL, AV_LOG_ERROR, "Cannot create audio amix filter\n"); - goto fail; - } - - abuffersink = avfilter_get_by_name("abuffersink"); - if (!abuffersink) { - fprintf(stderr, "Could not find the abuffersink filter.\n"); - err = AVERROR_FILTER_NOT_FOUND; - goto fail; - } - - abuffersink_ctx = avfilter_graph_alloc_filter(filter_graph, abuffersink, "sink"); - if (!abuffersink_ctx) { - fprintf(stderr, "Could not allocate the abuffersink instance.\n"); - err = AVERROR(ENOMEM); - goto fail; - } - - err = avfilter_init_str(abuffersink_ctx, NULL); - if (err < 0) { - fprintf(stderr, "Could not initialize the abuffersink instance.\n"); - goto fail; - } - - err = 0; - for(size_t i = 0; i < src_filter_ctx.size(); ++i) { - AVFilterContext *src_ctx = src_filter_ctx[i]; - if (err >= 0) - err = avfilter_link(src_ctx, 0, mix_ctx, i); - } - if (err >= 0) - err = avfilter_link(mix_ctx, 0, abuffersink_ctx, 0); - if (err < 0) { - av_log(NULL, AV_LOG_ERROR, "Error connecting filters\n"); - goto fail; - } - - err = avfilter_graph_config(filter_graph, NULL); - if (err < 0) { - av_log(NULL, AV_LOG_ERROR, "Error configuring the filter graph\n"); - goto fail; - } - - /* Make sure the sink always outputs frames with the exact amount of samples the audio encoder wants, - otherwise the audio encoder rejects the frame and that piece of audio is lost */ - av_buffersink_set_frame_size(abuffersink_ctx, audio_codec_context->frame_size); - - *graph = filter_graph; - *sink = abuffersink_ctx; - - return 0; - -fail: - avfilter_graph_free(&filter_graph); - src_filter_ctx.clear(); // possibly unnecessary? - return err; -} - static void list_system_info(bool wayland) { printf("display_server|%s\n", wayland ? "wayland" : "x11"); bool supports_app_audio = false; @@ -1149,101 +999,7 @@ static void capture_image_to_file(args_parser &arg_parser, gsr_egl *egl, gsr_win // OH, YOU MISSPELLED THE AUDIO INPUT? FUCK YOU // Should use amix if more than 1 audio device and 0 application audio, merged /* Returns -1 if none is available */ -static std::vector create_device_audio_inputs(const gsr_merged_audio_inputs *merged_audio_inputs, AVCodecContext *audio_codec_context, int num_channels, double num_audio_frames_shift, std::vector &src_filter_ctx, bool use_amix) { - std::vector audio_track_audio_devices; - for(size_t i = 0; i < merged_audio_inputs->num_items; ++i) { - const gsr_audio_input &audio_input = merged_audio_inputs->items[i]; - AVFilterContext *src_ctx = nullptr; - if(use_amix) - src_ctx = src_filter_ctx[i]; - - AudioDeviceData audio_device; - audio_device.audio_input = audio_input; - audio_device.src_filter_ctx = src_ctx; - - if(audio_input.name[0] == '\0') { - audio_device.sound_device.handle = NULL; - audio_device.sound_device.frames = 0; - } else { - char description[GSR_AUDIO_INPUT_NAME_MAX_SIZE + 8]; - snprintf(description, sizeof(description), "gsr-%s", audio_input.name); - if(sound_device_get_by_name(&audio_device.sound_device, description, audio_input.name, description, num_channels, audio_codec_context->frame_size, audio_codec_context_get_audio_format(audio_codec_context)) != 0) { - gsr_log(GSR_LOG_LEVEL_ERROR, "failed to get \"%s\" audio device", audio_input.name); - _exit(1); - } - } - - audio_device.frame = create_audio_frame(audio_codec_context); - if(!audio_device.frame) - _exit(1); - audio_device.frame->pts = -audio_codec_context->frame_size * num_audio_frames_shift; - - audio_track_audio_devices.push_back(std::move(audio_device)); - } - return audio_track_audio_devices; -} - #ifdef GSR_APP_AUDIO -static AudioDeviceData create_application_audio_audio_input(const gsr_merged_audio_inputs &merged_audio_inputs, AVCodecContext *audio_codec_context, int num_channels, double num_audio_frames_shift, gsr_pipewire_audio *pipewire_audio) { - AudioDeviceData audio_device; - audio_device.frame = create_audio_frame(audio_codec_context); - if(!audio_device.frame) - _exit(1); - audio_device.frame->pts = -audio_codec_context->frame_size * num_audio_frames_shift; - - char random_str[8]; - if(!generate_random_characters_standard_alphabet(random_str, sizeof(random_str))) { - gsr_log(GSR_LOG_LEVEL_ERROR, "failed to generate random string"); - _exit(1); - } - - std::string combined_sink_name = "gsr-combined-"; - combined_sink_name.append(random_str, sizeof(random_str)); - combined_sink_name += ".monitor"; - - if(sound_device_get_by_name(&audio_device.sound_device, combined_sink_name.c_str(), "", "gpu-screen-recorder", num_channels, audio_codec_context->frame_size, audio_codec_context_get_audio_format(audio_codec_context)) != 0) { - gsr_log(GSR_LOG_LEVEL_ERROR, "failed to setup audio recording to combined sink"); - _exit(1); - } - - std::vector audio_devices_sources; - for(size_t i = 0; i < merged_audio_inputs.num_items; ++i) { - const gsr_audio_input &audio_input = merged_audio_inputs.items[i]; - if(audio_input.type == GSR_AUDIO_INPUT_TYPE_DEVICE) - audio_devices_sources.push_back(audio_input.name); - } - - bool app_audio_inverted = false; - std::vector app_names; - for(size_t i = 0; i < merged_audio_inputs.num_items; ++i) { - const gsr_audio_input &audio_input = merged_audio_inputs.items[i]; - if(audio_input.type == GSR_AUDIO_INPUT_TYPE_APPLICATION) { - app_names.push_back(audio_input.name); - app_audio_inverted = audio_input.inverted; - } - } - - if(!audio_devices_sources.empty()) { - if(!gsr_pipewire_audio_add_link_from_sources_to_stream(pipewire_audio, audio_devices_sources.data(), audio_devices_sources.size(), combined_sink_name.c_str())) { - gsr_log(GSR_LOG_LEVEL_ERROR, "failed to add application audio link"); - _exit(1); - } - } - - if(app_audio_inverted) { - if(!gsr_pipewire_audio_add_link_from_apps_to_stream_inverted(pipewire_audio, app_names.data(), app_names.size(), combined_sink_name.c_str())) { - gsr_log(GSR_LOG_LEVEL_ERROR, "failed to add application audio link"); - _exit(1); - } - } else { - if(!gsr_pipewire_audio_add_link_from_apps_to_stream(pipewire_audio, app_names.data(), app_names.size(), combined_sink_name.c_str())) { - gsr_log(GSR_LOG_LEVEL_ERROR, "failed to add application audio link"); - _exit(1); - } - } - - return audio_device; -} #endif static bool get_image_format_from_filename(const char *filename, gsr_image_format *image_format) { @@ -1619,7 +1375,6 @@ int main(int argc, char **argv) { } AVStream *video_stream = nullptr; - std::vector audio_tracks; if(arg_parser.settings.video_encoder == GSR_VIDEO_ENCODER_HW_CPU && arg_parser.settings.video_codec != (gsr_video_codec)GSR_VIDEO_CODEC_AUTO && arg_parser.settings.video_codec != GSR_VIDEO_CODEC_H264) { gsr_log(GSR_LOG_LEVEL_ERROR, "-encoder cpu was specified but a codec other than h264 was specified. -encoder cpu supports only h264 at the moment"); @@ -1652,6 +1407,10 @@ int main(int argc, char **argv) { video_frame->chroma_location = video_codec_context->chroma_sample_location; const size_t estimated_replay_buffer_packets = calculate_estimated_replay_buffer_packets(arg_parser.settings.replay_buffer_size_secs, arg_parser.settings.fps, arg_parser.settings.audio_codec, &requested_audio_inputs); + gsr_recording_clock *recording_clock = gsr_recording_clock_create(); + if(!recording_clock) + _exit(1); + gsr_encoder encoder; if(!gsr_encoder_init(&encoder, arg_parser.settings.replay_storage, estimated_replay_buffer_packets, arg_parser.settings.replay_buffer_size_secs, arg_parser.settings.filename)) { gsr_log(GSR_LOG_LEVEL_ERROR, "failed to create encoder"); @@ -1716,6 +1475,10 @@ int main(int argc, char **argv) { } } + gsr_audio_capture audio_capture; + if(gsr_audio_capture_init(&audio_capture, &encoder, recording_clock, &running) != GSR_ERROR_OK) + _exit(1); + int audio_max_frame_size = 1024; int audio_stream_index = VIDEO_STREAM_INDEX + 1; for(size_t audio_track_index = 0; audio_track_index < requested_audio_inputs.num_items; ++audio_track_index) { @@ -1732,7 +1495,7 @@ int main(int argc, char **argv) { gsr_log(GSR_LOG_LEVEL_ERROR, "added too many audio sources"); } - if(audio_stream && !merged_audio_inputs.track_name[0] == '\0' && !arg_parser.settings.exclude_metadata) + if(audio_stream && merged_audio_inputs.track_name[0] != '\0' && !arg_parser.settings.exclude_metadata) av_dict_set(&audio_stream->metadata, "title", merged_audio_inputs.track_name, 0); if(!open_audio(audio_codec_context, arg_parser.settings.ffmpeg_audio_opts)) @@ -1748,12 +1511,16 @@ int main(int argc, char **argv) { //audio_frame->sample_rate = audio_codec_context->sample_rate; - std::vector src_filter_ctx; + AVFilterContext *src_filter_ctx[GSR_MAX_AUDIO_SOURCES_PER_TRACK]; AVFilterGraph *graph = nullptr; AVFilterContext *sink = nullptr; if(use_amix) { - int err = init_filter_graph(audio_codec_context, &graph, &sink, src_filter_ctx, merged_audio_inputs.num_items); - if(err < 0) { + if(merged_audio_inputs.num_items > GSR_MAX_AUDIO_SOURCES_PER_TRACK) { + gsr_log(GSR_LOG_LEVEL_ERROR, "too many audio sources for one audio track, the maximum is %d", GSR_MAX_AUDIO_SOURCES_PER_TRACK); + _exit(1); + } + + if(gsr_audio_init_filter_graph(audio_codec_context, &graph, &sink, src_filter_ctx, merged_audio_inputs.num_items) < 0) { gsr_log(GSR_LOG_LEVEL_ERROR, "failed to create audio filter"); _exit(1); } @@ -1767,28 +1534,34 @@ int main(int argc, char **argv) { const double audio_startup_time_seconds = force_no_audio_offset ? 0 : audio_codec_get_desired_delay(arg_parser.settings.audio_codec, arg_parser.settings.fps);// * ((double)audio_codec_context->frame_size / 1024.0); const double num_audio_frames_shift = audio_startup_time_seconds / timeout_sec; - std::vector audio_track_audio_devices; + gsr_audio_track audio_track; + memset(&audio_track, 0, sizeof(audio_track)); + snprintf(audio_track.name, sizeof(audio_track.name), "%s", merged_audio_inputs.track_name); + audio_track.codec_context = audio_codec_context; + audio_track.graph = graph; + audio_track.sink = sink; + audio_track.stream_index = audio_stream_index; + audio_track.pts = -audio_codec_context->frame_size * num_audio_frames_shift; + + int audio_track_result = GSR_ERROR_OK; if(gsr_audio_inputs_has_app_audio(&merged_audio_inputs)) { assert(!use_amix); #ifdef GSR_APP_AUDIO - audio_track_audio_devices.push_back(create_application_audio_audio_input(merged_audio_inputs, audio_codec_context, num_channels, num_audio_frames_shift, &pipewire_audio)); + audio_track_result = gsr_audio_track_init_application_input(&audio_track, &merged_audio_inputs, audio_codec_context, num_channels, num_audio_frames_shift, &pipewire_audio); #endif } else { - audio_track_audio_devices = create_device_audio_inputs(&merged_audio_inputs, audio_codec_context, num_channels, num_audio_frames_shift, src_filter_ctx, use_amix); + audio_track_result = gsr_audio_track_init_device_inputs(&audio_track, &merged_audio_inputs, audio_codec_context, num_channels, num_audio_frames_shift, src_filter_ctx, use_amix); } - AudioTrack audio_track; - audio_track.name = merged_audio_inputs.track_name; - audio_track.codec_context = audio_codec_context; - audio_track.audio_devices = std::move(audio_track_audio_devices); - audio_track.graph = graph; - audio_track.sink = sink; - audio_track.stream_index = audio_stream_index; - audio_track.pts = -audio_codec_context->frame_size * num_audio_frames_shift; - audio_tracks.push_back(std::move(audio_track)); + if(audio_track_result != GSR_ERROR_OK) + _exit(gsr_error_to_exit_code(audio_track_result)); + + if(!gsr_audio_capture_add_track(&audio_capture, &audio_track)) + _exit(1); ++audio_stream_index; - audio_max_frame_size = std::max(audio_max_frame_size, audio_codec_context->frame_size); + if(audio_codec_context->frame_size > audio_max_frame_size) + audio_max_frame_size = audio_codec_context->frame_size; } //av_dump_format(av_format_context, 0, filename, 1); @@ -1810,230 +1583,17 @@ int main(int argc, char **argv) { int damage_fps_counter = 0; bool paused = false; - std::atomic paused_time_offset(0.0); - double paused_time_start = 0.0; bool replay_recording = false; RecordingStartResult replay_recording_start_result; std::vector replay_recording_items; std::string replay_recording_filepath; bool force_iframe_frame = false; // Only needed for video since audio frames are always iframes - std::mutex audio_filter_mutex; + gsr_recording_clock_start(recording_clock); + const double record_start_time = gsr_recording_clock_get_start_time(recording_clock); - const double record_start_time = clock_get_monotonic_seconds(); - - const size_t audio_buffer_size = audio_max_frame_size * 4 * 2; // max 4 bytes/sample, 2 channels - uint8_t *empty_audio = (uint8_t*)malloc(audio_buffer_size); - if(!empty_audio) { - gsr_log(GSR_LOG_LEVEL_ERROR, "failed to create empty audio"); + if(gsr_audio_capture_start(&audio_capture, audio_max_frame_size, uses_amix) != GSR_ERROR_OK) _exit(1); - } - memset(empty_audio, 0, audio_buffer_size); - - for(AudioTrack &audio_track : audio_tracks) { - for(AudioDeviceData &audio_device : audio_track.audio_devices) { - audio_device.thread = std::thread([&]() mutable { - const AVSampleFormat sound_device_sample_format = audio_format_to_sample_format(audio_codec_context_get_audio_format(audio_track.codec_context)); - // TODO: Always do conversion for now. This fixes issue with stuttering audio on pulseaudio with opus + multiple audio sources merged - const bool needs_audio_conversion = true;//audio_track.codec_context->sample_fmt != sound_device_sample_format; - SwrContext *swr = nullptr; - if(needs_audio_conversion) { - swr = swr_alloc(); - if(!swr) { - fprintf(stderr, "Failed to create SwrContext\n"); - _exit(1); - } - #if LIBAVUTIL_VERSION_MAJOR <= 56 - av_opt_set_channel_layout(swr, "in_channel_layout", AV_CH_LAYOUT_STEREO, 0); - av_opt_set_channel_layout(swr, "out_channel_layout", AV_CH_LAYOUT_STEREO, 0); - #elif LIBAVUTIL_VERSION_MAJOR >= 59 - av_opt_set_chlayout(swr, "in_chlayout", &audio_track.codec_context->ch_layout, 0); - av_opt_set_chlayout(swr, "out_chlayout", &audio_track.codec_context->ch_layout, 0); - #else - av_opt_set_chlayout(swr, "in_channel_layout", &audio_track.codec_context->ch_layout, 0); - av_opt_set_chlayout(swr, "out_channel_layout", &audio_track.codec_context->ch_layout, 0); - #endif - av_opt_set_int(swr, "in_sample_rate", audio_track.codec_context->sample_rate, 0); - av_opt_set_int(swr, "out_sample_rate", audio_track.codec_context->sample_rate, 0); - av_opt_set_sample_fmt(swr, "in_sample_fmt", sound_device_sample_format, 0); - av_opt_set_sample_fmt(swr, "out_sample_fmt", audio_track.codec_context->sample_fmt, 0); - swr_init(swr); - } - - const double audio_fps = (double)audio_track.codec_context->sample_rate / (double)audio_track.codec_context->frame_size; - const int64_t timeout_ms = std::round(1000.0 / audio_fps); - const double timeout_sec = 1000.0 / audio_fps / 1000.0; - int64_t num_received_frames = 0; - - // The sound device is opened before the recording starts, so it can contain old audio from before the recording started. - // Discard it so the recording doesn't start with old audio. - if(audio_device.sound_device.handle) - sound_device_flush(&audio_device.sound_device); - - while(running) { - void *sound_buffer; - int sound_buffer_size = -1; - const double time_before_read_seconds = clock_get_monotonic_seconds(); - if(audio_device.sound_device.handle) { - // TODO: use this instead of calculating time to read. But this can fluctuate and we dont want to go back in time, - // also it's 0.0 for some users??? - double latency_seconds = 0.0; - sound_buffer_size = sound_device_read_next_chunk(&audio_device.sound_device, &sound_buffer, timeout_sec * 2.0, &latency_seconds); - } - - const bool got_audio_data = sound_buffer_size >= 0; - //fprintf(stderr, "got audio data: %s\n", got_audio_data ? "yes" : "no"); - //fprintf(stderr, "time to read: %f, %s, %f\n", time_to_read_seconds, got_audio_data ? "yes" : "no", timeout_sec); - const double this_audio_frame_time = clock_get_monotonic_seconds() - paused_time_offset; - - if(paused) { - if(!audio_device.sound_device.handle) - av_usleep(timeout_ms * 1000); - - continue; - } - - int ret = av_frame_make_writable(audio_device.frame); - if (ret < 0) { - fprintf(stderr, "Failed to make audio frame writable\n"); - break; - } - - // TODO: Is this |received_audio_time| really correct? - const int64_t num_expected_frames = std::floor((this_audio_frame_time - record_start_time) / timeout_sec); - int64_t num_missing_frames = std::max((int64_t)0LL, num_expected_frames - num_received_frames); - - if(got_audio_data) - num_missing_frames = std::max((int64_t)0LL, num_missing_frames - 1); - - if(!audio_device.sound_device.handle) - num_missing_frames = std::max((int64_t)1, num_missing_frames); - - // Fucking hell is there a better way to do this? I JUST WANT TO KEEP VIDEO AND AUDIO SYNCED HOLY FUCK I WANT TO KILL MYSELF NOW. - // THIS PIECE OF SHIT WANTS EMPTY FRAMES OTHERWISE VIDEO PLAYS TOO FAST TO KEEP UP WITH AUDIO OR THE AUDIO PLAYS TOO EARLY. - // BUT WE CANT USE DELAYS TO GIVE DUMMY DATA BECAUSE PULSEAUDIO MIGHT GIVE AUDIO A BIG DELAYED!!! - // This garbage is needed because we want to produce constant frame rate videos instead of variable frame rate - // videos because bad software such as video editing software and VLC do not support variable frame rate software, - // despite nvidia shadowplay and xbox game bar producing variable frame rate videos. - // So we have to make sure we produce frames at the same relative rate as the video. - if((num_missing_frames >= 1 && got_audio_data) || num_missing_frames >= 5 || !audio_device.sound_device.handle) { - // Fill the missing frames with silence. Duplicating the previous audio frame to fill the gap instead - // sounds like a stutter and it's especially noticeable at the start of the recording when the audio device - // hasn't started to deliver audio at a stable rate yet, which repeats the first audio frame multiple times. - if(needs_audio_conversion) - swr_convert(swr, &audio_device.frame->data[0], audio_track.codec_context->frame_size, (const uint8_t**)&empty_audio, audio_track.codec_context->frame_size); - else - audio_device.frame->data[0] = empty_audio; - - // TODO: Check if duplicate frame can be saved just by writing it with a different pts instead of sending it again - std::lock_guard lock(audio_filter_mutex); - for(int i = 0; i < num_missing_frames; ++i) { - if(audio_track.graph) { - // TODO: av_buffersrc_add_frame - if(av_buffersrc_write_frame(audio_device.src_filter_ctx, audio_device.frame) < 0) { - gsr_log(GSR_LOG_LEVEL_ERROR, "failed to add audio frame to filter"); - } - } else { - ret = avcodec_send_frame(audio_track.codec_context, audio_device.frame); - if(ret >= 0) { - // TODO: Move to separate thread because this could write to network (for example when livestreaming) - gsr_encoder_receive_packets(&encoder, audio_track.codec_context, audio_device.frame->pts, audio_track.stream_index); - } else { - fprintf(stderr, "Failed to encode audio!\n"); - } - audio_track.pts += audio_track.codec_context->frame_size; - } - - audio_device.frame->pts += audio_track.codec_context->frame_size; - num_received_frames++; - } - } - - if(!audio_device.sound_device.handle) { - av_usleep(timeout_ms * 1000); - } else if(got_audio_data) { - // The frame has to be made writable again if the frame was already sent to the audio filter above (when filling missing frames) - // because the audio filter only references the frame data instead of copying it. Without this the sent frames data would be - // overwritten with the audio data below, causing the audio to repeat instead of the missing frames being silent. - ret = av_frame_make_writable(audio_device.frame); - if (ret < 0) { - fprintf(stderr, "Failed to make audio frame writable\n"); - break; - } - - // TODO: Instead of converting audio, get float audio from alsa. Or does alsa do conversion internally to get this format? - if(needs_audio_conversion) - swr_convert(swr, &audio_device.frame->data[0], audio_track.codec_context->frame_size, (const uint8_t**)&sound_buffer, audio_track.codec_context->frame_size); - else - audio_device.frame->data[0] = (uint8_t*)sound_buffer; - - std::lock_guard lock(audio_filter_mutex); - - if(audio_track.graph) { - // TODO: av_buffersrc_add_frame - if(av_buffersrc_write_frame(audio_device.src_filter_ctx, audio_device.frame) < 0) { - gsr_log(GSR_LOG_LEVEL_ERROR, "failed to add audio frame to filter"); - } - } else { - ret = avcodec_send_frame(audio_track.codec_context, audio_device.frame); - if(ret >= 0) { - // TODO: Move to separate thread because this could write to network (for example when livestreaming) - gsr_encoder_receive_packets(&encoder, audio_track.codec_context, audio_device.frame->pts, audio_track.stream_index); - } else { - fprintf(stderr, "Failed to encode audio!\n"); - } - audio_track.pts += audio_track.codec_context->frame_size; - } - - audio_device.frame->pts += audio_track.codec_context->frame_size; - num_received_frames++; - } else { - // TODO: Maybe sleep for time_to_sleep_until_next_frame/4? for better latency - const double time_after_read_seconds = clock_get_monotonic_seconds(); - const double time_to_read_seconds = time_after_read_seconds - time_before_read_seconds; - const double time_to_sleep_until_next_frame = timeout_sec - time_to_read_seconds; - if(time_to_sleep_until_next_frame > 0.0) - av_usleep(time_to_sleep_until_next_frame * 1000ULL * 1000ULL); - } - } - - if(swr) - swr_free(&swr); - }); - } - } - - std::thread amix_thread; - if(uses_amix) { - amix_thread = std::thread([&]() { - AVFrame *aframe = av_frame_alloc(); - while(running) { - { - std::lock_guard lock(audio_filter_mutex); - for(AudioTrack &audio_track : audio_tracks) { - if(!audio_track.sink) - continue; - - int err = 0; - while ((err = av_buffersink_get_frame(audio_track.sink, aframe)) >= 0) { - aframe->pts = audio_track.pts; - err = avcodec_send_frame(audio_track.codec_context, aframe); - if(err >= 0){ - // TODO: Move to separate thread because this could write to network (for example when livestreaming) - gsr_encoder_receive_packets(&encoder, audio_track.codec_context, aframe->pts, audio_track.stream_index); - } else { - fprintf(stderr, "Failed to encode audio!\n"); - } - av_frame_unref(aframe); - audio_track.pts += audio_track.codec_context->frame_size; - } - } - } - av_usleep(5 * 1000); // 5 milliseconds - } - av_frame_free(&aframe); - }); - } // Set update_fps to 24 to test if duplicate/delayed frames cause video/audio desync or too fast/slow video. //const double update_fps = fps + 190; @@ -2146,7 +1706,7 @@ int main(int argc, char **argv) { damage_fps_counter = 0; } - const double this_video_frame_time = clock_get_monotonic_seconds() - paused_time_offset; + const double this_video_frame_time = gsr_recording_clock_get_time(recording_clock); const int64_t expected_frames = std::floor((this_video_frame_time - record_start_time) / target_fps); const int64_t num_missed_frames = expected_frames - video_pts_counter; @@ -2169,8 +1729,8 @@ int main(int argc, char **argv) { if(video_source.capture->capture_has_synchronous_task) { capture_has_synchronous_task = video_source.capture->capture_has_synchronous_task(video_source.capture); if(capture_has_synchronous_task) { - paused_time_start = clock_get_monotonic_seconds(); paused = true; + gsr_recording_clock_set_paused(recording_clock, true); } } } @@ -2202,8 +1762,8 @@ int main(int argc, char **argv) { } if(capture_has_synchronous_task) { - paused_time_offset = paused_time_offset + (clock_get_monotonic_seconds() - paused_time_start); paused = false; + gsr_recording_clock_set_paused(recording_clock, false); } gsr_egl_swap_buffers(&egl); @@ -2250,17 +1810,10 @@ int main(int argc, char **argv) { } if(toggle_pause == 1 && !arg_parser.settings.is_replaying) { - const bool new_paused_state = !paused; - if(new_paused_state) { - paused_time_start = clock_get_monotonic_seconds(); - fprintf(stderr, "Paused\n"); - } else { - paused_time_offset = paused_time_offset + (clock_get_monotonic_seconds() - paused_time_start); - fprintf(stderr, "Unpaused\n"); - } - - toggle_pause = 0; paused = !paused; + gsr_recording_clock_set_paused(recording_clock, paused); + gsr_log(GSR_LOG_LEVEL_INFO, paused ? "Paused" : "Unpaused"); + toggle_pause = 0; } if(toggle_replay_recording && !arg_parser.settings.replay_recording_directory) { @@ -2273,10 +1826,10 @@ int main(int argc, char **argv) { toggle_replay_recording = 0; const bool new_replay_recording_state = !replay_recording; if(new_replay_recording_state) { - std::lock_guard lock(audio_filter_mutex); + gsr_audio_capture_lock_filter(&audio_capture); replay_recording_items.clear(); replay_recording_filepath = create_new_recording_filepath_from_timestamp(arg_parser.settings.replay_recording_directory, "Video", file_extension, arg_parser.settings.date_folders); - replay_recording_start_result = start_recording_create_streams(replay_recording_filepath.c_str(), arg_parser, video_codec_context, audio_tracks, hdr, video_sources); + replay_recording_start_result = start_recording_create_streams(replay_recording_filepath.c_str(), arg_parser, video_codec_context, &audio_capture, hdr, video_sources); if(replay_recording_start_result.av_format_context) { const size_t video_recording_destination_id = gsr_encoder_add_recording_destination(&encoder, video_codec_context, replay_recording_start_result.av_format_context, replay_recording_start_result.video_stream, video_frame->pts); if(arg_parser.settings.write_first_frame_ts && video_recording_destination_id != (size_t)-1) { @@ -2343,7 +1896,7 @@ int main(int argc, char **argv) { save_replay_seconds = 0; save_replay_output_filepath.clear(); - const bool replay_start_result = save_replay_async(video_codec_context, VIDEO_STREAM_INDEX, audio_tracks, &encoder, arg_parser, file_extension, arg_parser.settings.date_folders, hdr, video_sources, current_save_replay_seconds); + const bool replay_start_result = save_replay_async(video_codec_context, VIDEO_STREAM_INDEX, &audio_capture, &encoder, arg_parser, file_extension, arg_parser.settings.date_folders, hdr, video_sources, current_save_replay_seconds); if(!replay_start_result) { printf("gsr error: Failed to save replay\n"); fflush(stdout); @@ -2356,7 +1909,7 @@ int main(int argc, char **argv) { } } - const double time_at_frame_end = clock_get_monotonic_seconds() - paused_time_offset; + const double time_at_frame_end = gsr_recording_clock_get_time(recording_clock); const double time_elapsed_total = time_at_frame_end - record_start_time; const int64_t frames_elapsed = std::floor(time_elapsed_total / target_fps); const double time_at_next_frame = (frames_elapsed + 1) * target_fps; @@ -2409,15 +1962,7 @@ int main(int argc, char **argv) { } } - for(AudioTrack &audio_track : audio_tracks) { - for(auto &audio_device : audio_track.audio_devices) { - audio_device.thread.join(); - sound_device_close(&audio_device.sound_device); - } - } - - if(amix_thread.joinable()) - amix_thread.join(); + gsr_audio_capture_join_threads(&audio_capture); // TODO: Replace this with start_recording_create_steams if(!arg_parser.settings.is_replaying && av_write_trailer(av_format_context) != 0) { @@ -2451,7 +1996,6 @@ int main(int argc, char **argv) { //gsr_window_destroy(&window); //av_frame_free(&video_frame); - free(empty_audio); 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 diff --git a/src/recorder/audio_capture.c b/src/recorder/audio_capture.c new file mode 100644 index 0000000..ee1c75f --- /dev/null +++ b/src/recorder/audio_capture.c @@ -0,0 +1,610 @@ +#include "../../include/recorder/audio_capture.h" +#include "../../include/recorder/error.h" +#include "../../include/recorder/audio_codec.h" +#include "../../include/utils.h" +#include "../../include/log.h" + +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +int gsr_audio_init_filter_graph(AVCodecContext *audio_codec_context, AVFilterGraph **graph, AVFilterContext **sink, AVFilterContext **src_filter_ctx, size_t num_sources) { + char ch_layout[64]; + int err = 0; + ch_layout[0] = '\0'; + + // C89-style variable declaration to + // avoid problems because of goto + AVFilterGraph* filter_graph = NULL; + AVFilterContext* mix_ctx = NULL; + + const AVFilter* mix_filter = NULL; + const AVFilter* abuffersink = NULL; + AVFilterContext* abuffersink_ctx = NULL; + char args[512] = { 0 }; +#if LIBAVFILTER_VERSION_INT >= AV_VERSION_INT(7, 107, 100) + bool normalize = false; +#endif + + filter_graph = avfilter_graph_alloc(); + if (!filter_graph) { + gsr_log(GSR_LOG_LEVEL_ERROR, "Unable to create filter graph"); + err = AVERROR(ENOMEM); + goto fail; + } + + for(size_t i = 0; i < num_sources; ++i) { + const AVFilter *abuffer = avfilter_get_by_name("abuffer"); + if (!abuffer) { + gsr_log(GSR_LOG_LEVEL_ERROR, "Could not find the abuffer filter"); + err = AVERROR_FILTER_NOT_FOUND; + goto fail; + } + + AVFilterContext *abuffer_ctx = avfilter_graph_alloc_filter(filter_graph, abuffer, NULL); + if (!abuffer_ctx) { + gsr_log(GSR_LOG_LEVEL_ERROR, "Could not allocate the abuffer instance"); + err = AVERROR(ENOMEM); + goto fail; + } + + #if LIBAVCODEC_VERSION_MAJOR < 60 + av_get_channel_layout_string(ch_layout, sizeof(ch_layout), 0, AV_CH_LAYOUT_STEREO); + #else + av_channel_layout_describe(&audio_codec_context->ch_layout, ch_layout, sizeof(ch_layout)); + #endif + av_opt_set (abuffer_ctx, "channel_layout", ch_layout, AV_OPT_SEARCH_CHILDREN); + av_opt_set (abuffer_ctx, "sample_fmt", av_get_sample_fmt_name(audio_codec_context->sample_fmt), AV_OPT_SEARCH_CHILDREN); + av_opt_set_q (abuffer_ctx, "time_base", audio_codec_context->time_base, AV_OPT_SEARCH_CHILDREN); + av_opt_set_int(abuffer_ctx, "sample_rate", audio_codec_context->sample_rate, AV_OPT_SEARCH_CHILDREN); + av_opt_set_int(abuffer_ctx, "bit_rate", audio_codec_context->bit_rate, AV_OPT_SEARCH_CHILDREN); + + err = avfilter_init_str(abuffer_ctx, NULL); + if (err < 0) { + gsr_log(GSR_LOG_LEVEL_ERROR, "Could not initialize the abuffer filter"); + goto fail; + } + + src_filter_ctx[i] = abuffer_ctx; + } + + mix_filter = avfilter_get_by_name("amix"); + if (!mix_filter) { + av_log(NULL, AV_LOG_ERROR, "Could not find the mix filter.\n"); + err = AVERROR_FILTER_NOT_FOUND; + goto fail; + } + +#if LIBAVFILTER_VERSION_INT >= AV_VERSION_INT(7, 107, 100) + snprintf(args, sizeof(args), "inputs=%d:normalize=%s", (int)num_sources, normalize ? "true" : "false"); +#else + snprintf(args, sizeof(args), "inputs=%d", (int)num_sources); + gsr_log(GSR_LOG_LEVEL_WARNING, "your ffmpeg version doesn't support disabling normalizing of mixed audio. Volume might be lower than expected"); +#endif + + err = avfilter_graph_create_filter(&mix_ctx, mix_filter, "amix", args, NULL, filter_graph); + if (err < 0) { + av_log(NULL, AV_LOG_ERROR, "Cannot create audio amix filter\n"); + goto fail; + } + + abuffersink = avfilter_get_by_name("abuffersink"); + if (!abuffersink) { + gsr_log(GSR_LOG_LEVEL_ERROR, "Could not find the abuffersink filter"); + err = AVERROR_FILTER_NOT_FOUND; + goto fail; + } + + abuffersink_ctx = avfilter_graph_alloc_filter(filter_graph, abuffersink, "sink"); + if (!abuffersink_ctx) { + gsr_log(GSR_LOG_LEVEL_ERROR, "Could not allocate the abuffersink instance"); + err = AVERROR(ENOMEM); + goto fail; + } + + err = avfilter_init_str(abuffersink_ctx, NULL); + if (err < 0) { + gsr_log(GSR_LOG_LEVEL_ERROR, "Could not initialize the abuffersink instance"); + goto fail; + } + + err = 0; + for(size_t i = 0; i < num_sources; ++i) { + AVFilterContext *src_ctx = src_filter_ctx[i]; + if (err >= 0) + err = avfilter_link(src_ctx, 0, mix_ctx, i); + } + if (err >= 0) + err = avfilter_link(mix_ctx, 0, abuffersink_ctx, 0); + if (err < 0) { + av_log(NULL, AV_LOG_ERROR, "Error connecting filters\n"); + goto fail; + } + + err = avfilter_graph_config(filter_graph, NULL); + if (err < 0) { + av_log(NULL, AV_LOG_ERROR, "Error configuring the filter graph\n"); + goto fail; + } + + /* Make sure the sink always outputs frames with the exact amount of samples the audio encoder wants, + otherwise the audio encoder rejects the frame and that piece of audio is lost */ + av_buffersink_set_frame_size(abuffersink_ctx, audio_codec_context->frame_size); + + *graph = filter_graph; + *sink = abuffersink_ctx; + + return 0; + +fail: + avfilter_graph_free(&filter_graph); + memset(src_filter_ctx, 0, num_sources * sizeof(AVFilterContext*)); // possibly unnecessary? + return err; +} + +static void* audio_device_thread(void *userdata) { + const gsr_audio_device_thread_userdata *thread_userdata = userdata; + gsr_audio_capture *self = thread_userdata->audio_capture; + gsr_audio_track *track = thread_userdata->track; + gsr_audio_device_capture *device = thread_userdata->device; + gsr_recording_clock *clock = self->clock; + const volatile sig_atomic_t *running = self->running; + + const enum AVSampleFormat sound_device_sample_format = audio_format_to_sample_format(audio_codec_context_get_audio_format(track->codec_context)); + /* TODO: Always do conversion for now. This fixes issue with stuttering audio on pulseaudio with opus + multiple audio sources merged */ + const bool needs_audio_conversion = true; + SwrContext *swr = NULL; + if(needs_audio_conversion) { + swr = swr_alloc(); + if(!swr) { + gsr_log(GSR_LOG_LEVEL_ERROR, "failed to create SwrContext"); + return NULL; + } + #if LIBAVUTIL_VERSION_MAJOR <= 56 + av_opt_set_channel_layout(swr, "in_channel_layout", AV_CH_LAYOUT_STEREO, 0); + av_opt_set_channel_layout(swr, "out_channel_layout", AV_CH_LAYOUT_STEREO, 0); + #elif LIBAVUTIL_VERSION_MAJOR >= 59 + av_opt_set_chlayout(swr, "in_chlayout", &track->codec_context->ch_layout, 0); + av_opt_set_chlayout(swr, "out_chlayout", &track->codec_context->ch_layout, 0); + #else + av_opt_set_chlayout(swr, "in_channel_layout", &track->codec_context->ch_layout, 0); + av_opt_set_chlayout(swr, "out_channel_layout", &track->codec_context->ch_layout, 0); + #endif + av_opt_set_int(swr, "in_sample_rate", track->codec_context->sample_rate, 0); + av_opt_set_int(swr, "out_sample_rate", track->codec_context->sample_rate, 0); + av_opt_set_sample_fmt(swr, "in_sample_fmt", sound_device_sample_format, 0); + av_opt_set_sample_fmt(swr, "out_sample_fmt", track->codec_context->sample_fmt, 0); + swr_init(swr); + } + + const double audio_fps = (double)track->codec_context->sample_rate / (double)track->codec_context->frame_size; + const int64_t timeout_ms = llround(1000.0 / audio_fps); + const double timeout_sec = 1000.0 / audio_fps / 1000.0; + int64_t num_received_frames = 0; + + /* The sound device is opened before the recording starts, so it can contain old audio from before the recording started. + Discard it so the recording doesn't start with old audio. */ + if(device->sound_device.handle) + sound_device_flush(&device->sound_device); + + while(*running) { + void *sound_buffer; + int sound_buffer_size = -1; + const double time_before_read_seconds = clock_get_monotonic_seconds(); + if(device->sound_device.handle) { + // TODO: use this instead of calculating time to read. But this can fluctuate and we dont want to go back in time, + // also it's 0.0 for some users??? + double latency_seconds = 0.0; + sound_buffer_size = sound_device_read_next_chunk(&device->sound_device, &sound_buffer, timeout_sec * 2.0, &latency_seconds); + } + + const bool got_audio_data = sound_buffer_size >= 0; + //fprintf(stderr, "got audio data: %s\n", got_audio_data ? "yes" : "no"); + //fprintf(stderr, "time to read: %f, %s, %f\n", time_to_read_seconds, got_audio_data ? "yes" : "no", timeout_sec); + const double this_audio_frame_time = gsr_recording_clock_get_time(clock); + + if(gsr_recording_clock_is_paused(clock)) { + if(!device->sound_device.handle) + av_usleep(timeout_ms * 1000); + + continue; + } + + int ret = av_frame_make_writable(device->frame); + if (ret < 0) { + gsr_log(GSR_LOG_LEVEL_ERROR, "Failed to make audio frame writable"); + break; + } + + // TODO: Is this |received_audio_time| really correct? + const int64_t num_expected_frames = floor((this_audio_frame_time - gsr_recording_clock_get_start_time(clock)) / timeout_sec); + int64_t num_missing_frames = num_expected_frames > num_received_frames ? num_expected_frames - num_received_frames : 0; + + if(got_audio_data) + num_missing_frames = num_missing_frames > 1 ? num_missing_frames - 1 : 0; + + if(!device->sound_device.handle) + num_missing_frames = num_missing_frames < 1 ? 1 : num_missing_frames; + + // Fucking hell is there a better way to do this? I JUST WANT TO KEEP VIDEO AND AUDIO SYNCED HOLY FUCK I WANT TO KILL MYSELF NOW. + // THIS PIECE OF SHIT WANTS EMPTY FRAMES OTHERWISE VIDEO PLAYS TOO FAST TO KEEP UP WITH AUDIO OR THE AUDIO PLAYS TOO EARLY. + // BUT WE CANT USE DELAYS TO GIVE DUMMY DATA BECAUSE PULSEAUDIO MIGHT GIVE AUDIO A BIG DELAYED!!! + // This garbage is needed because we want to produce constant frame rate videos instead of variable frame rate + // videos because bad software such as video editing software and VLC do not support variable frame rate software, + // despite nvidia shadowplay and xbox game bar producing variable frame rate videos. + // So we have to make sure we produce frames at the same relative rate as the video. + if((num_missing_frames >= 1 && got_audio_data) || num_missing_frames >= 5 || !device->sound_device.handle) { + // Fill the missing frames with silence. Duplicating the previous audio frame to fill the gap instead + // sounds like a stutter and it's especially noticeable at the start of the recording when the audio device + // hasn't started to deliver audio at a stable rate yet, which repeats the first audio frame multiple times. + if(needs_audio_conversion) + swr_convert(swr, &device->frame->data[0], track->codec_context->frame_size, (const uint8_t**)&self->empty_audio, track->codec_context->frame_size); + else + device->frame->data[0] = self->empty_audio; + + // TODO: Check if duplicate frame can be saved just by writing it with a different pts instead of sending it again + pthread_mutex_lock(&self->filter_mutex); + for(int i = 0; i < num_missing_frames; ++i) { + if(track->graph) { + // TODO: av_buffersrc_add_frame + if(av_buffersrc_write_frame(device->src_filter_ctx, device->frame) < 0) { + gsr_log(GSR_LOG_LEVEL_ERROR, "failed to add audio frame to filter"); + } + } else { + ret = avcodec_send_frame(track->codec_context, device->frame); + if(ret >= 0) { + // TODO: Move to separate thread because this could write to network (for example when livestreaming) + gsr_encoder_receive_packets(self->encoder, track->codec_context, device->frame->pts, track->stream_index); + } else { + gsr_log(GSR_LOG_LEVEL_ERROR, "Failed to encode audio"); + } + track->pts += track->codec_context->frame_size; + } + + device->frame->pts += track->codec_context->frame_size; + num_received_frames++; + } + pthread_mutex_unlock(&self->filter_mutex); + } + + if(!device->sound_device.handle) { + av_usleep(timeout_ms * 1000); + } else if(got_audio_data) { + // The frame has to be made writable again if the frame was already sent to the audio filter above (when filling missing frames) + // because the audio filter only references the frame data instead of copying it. Without this the sent frames data would be + // overwritten with the audio data below, causing the audio to repeat instead of the missing frames being silent. + ret = av_frame_make_writable(device->frame); + if (ret < 0) { + gsr_log(GSR_LOG_LEVEL_ERROR, "Failed to make audio frame writable"); + break; + } + + // TODO: Instead of converting audio, get float audio from alsa. Or does alsa do conversion internally to get this format? + if(needs_audio_conversion) + swr_convert(swr, &device->frame->data[0], track->codec_context->frame_size, (const uint8_t**)&sound_buffer, track->codec_context->frame_size); + else + device->frame->data[0] = (uint8_t*)sound_buffer; + + pthread_mutex_lock(&self->filter_mutex); + + if(track->graph) { + // TODO: av_buffersrc_add_frame + if(av_buffersrc_write_frame(device->src_filter_ctx, device->frame) < 0) { + gsr_log(GSR_LOG_LEVEL_ERROR, "failed to add audio frame to filter"); + } + } else { + ret = avcodec_send_frame(track->codec_context, device->frame); + if(ret >= 0) { + // TODO: Move to separate thread because this could write to network (for example when livestreaming) + gsr_encoder_receive_packets(self->encoder, track->codec_context, device->frame->pts, track->stream_index); + } else { + gsr_log(GSR_LOG_LEVEL_ERROR, "Failed to encode audio"); + } + track->pts += track->codec_context->frame_size; + } + + device->frame->pts += track->codec_context->frame_size; + num_received_frames++; + pthread_mutex_unlock(&self->filter_mutex); + } else { + // TODO: Maybe sleep for time_to_sleep_until_next_frame/4? for better latency + const double time_after_read_seconds = clock_get_monotonic_seconds(); + const double time_to_read_seconds = time_after_read_seconds - time_before_read_seconds; + const double time_to_sleep_until_next_frame = timeout_sec - time_to_read_seconds; + if(time_to_sleep_until_next_frame > 0.0) + av_usleep(time_to_sleep_until_next_frame * 1000ULL * 1000ULL); + } + } + + if(swr) + swr_free(&swr); + + return NULL; +} + +static void* amix_thread(void *userdata) { + gsr_audio_capture *self = userdata; + AVFrame *aframe = av_frame_alloc(); + while(*self->running) { + pthread_mutex_lock(&self->filter_mutex); + for(size_t i = 0; i < self->num_tracks; ++i) { + gsr_audio_track *track = &self->tracks[i]; + if(!track->sink) + continue; + + int err = 0; + while((err = av_buffersink_get_frame(track->sink, aframe)) >= 0) { + aframe->pts = track->pts; + err = avcodec_send_frame(track->codec_context, aframe); + if(err >= 0) { + /* TODO: Move to separate thread because this could write to network (for example when livestreaming) */ + gsr_encoder_receive_packets(self->encoder, track->codec_context, aframe->pts, track->stream_index); + } else { + gsr_log(GSR_LOG_LEVEL_ERROR, "Failed to encode audio"); + } + av_frame_unref(aframe); + track->pts += track->codec_context->frame_size; + } + } + pthread_mutex_unlock(&self->filter_mutex); + av_usleep(5 * 1000); /* 5 milliseconds */ + } + av_frame_free(&aframe); + return NULL; +} + +int gsr_audio_capture_init(gsr_audio_capture *self, gsr_encoder *encoder, gsr_recording_clock *clock, const volatile sig_atomic_t *running) { + memset(self, 0, sizeof(*self)); + self->encoder = encoder; + self->clock = clock; + self->running = running; + + if(pthread_mutex_init(&self->filter_mutex, NULL) != 0) { + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_audio_capture_init: failed to initialize mutex"); + return GSR_ERROR_GENERIC; + } + self->filter_mutex_initialized = true; + + return GSR_ERROR_OK; +} + +void gsr_audio_capture_deinit(gsr_audio_capture *self) { + gsr_audio_capture_join_threads(self); + + for(size_t i = 0; i < self->num_tracks; ++i) { + gsr_audio_track_deinit(&self->tracks[i]); + } + + if(self->tracks) { + free(self->tracks); + self->tracks = NULL; + } + self->num_tracks = 0; + self->capacity_tracks = 0; + + if(self->empty_audio) { + free(self->empty_audio); + self->empty_audio = NULL; + } + + if(self->filter_mutex_initialized) { + pthread_mutex_destroy(&self->filter_mutex); + self->filter_mutex_initialized = false; + } +} + +bool gsr_audio_capture_add_track(gsr_audio_capture *self, const gsr_audio_track *track) { + if(!gsr_array_ensure_capacity((void**)&self->tracks, self->num_tracks, &self->capacity_tracks, sizeof(gsr_audio_track))) + return false; + + self->tracks[self->num_tracks] = *track; + ++self->num_tracks; + return true; +} + +int gsr_audio_capture_start(gsr_audio_capture *self, int audio_max_frame_size, bool uses_amix) { + const size_t audio_buffer_size = audio_max_frame_size * 4 * 2; /* max 4 bytes/sample, 2 channels */ + self->empty_audio = calloc(1, audio_buffer_size); + if(!self->empty_audio) { + gsr_log(GSR_LOG_LEVEL_ERROR, "failed to create empty audio"); + return GSR_ERROR_GENERIC; + } + + for(size_t i = 0; i < self->num_tracks; ++i) { + gsr_audio_track *track = &self->tracks[i]; + for(size_t j = 0; j < track->num_audio_devices; ++j) { + gsr_audio_device_capture *device = &track->audio_devices[j]; + device->thread_userdata.audio_capture = self; + device->thread_userdata.track = track; + device->thread_userdata.device = device; + if(pthread_create(&device->thread, NULL, audio_device_thread, &device->thread_userdata) != 0) { + gsr_log(GSR_LOG_LEVEL_ERROR, "failed to create audio thread"); + return GSR_ERROR_GENERIC; + } + device->thread_created = true; + } + } + + if(uses_amix) { + if(pthread_create(&self->amix_thread, NULL, amix_thread, self) != 0) { + gsr_log(GSR_LOG_LEVEL_ERROR, "failed to create audio mix thread"); + return GSR_ERROR_GENERIC; + } + self->amix_thread_created = true; + } + + return GSR_ERROR_OK; +} + +void gsr_audio_capture_join_threads(gsr_audio_capture *self) { + for(size_t i = 0; i < self->num_tracks; ++i) { + gsr_audio_track *track = &self->tracks[i]; + for(size_t j = 0; j < track->num_audio_devices; ++j) { + gsr_audio_device_capture *device = &track->audio_devices[j]; + if(device->thread_created) { + pthread_join(device->thread, NULL); + device->thread_created = false; + } + } + } + + if(self->amix_thread_created) { + pthread_join(self->amix_thread, NULL); + self->amix_thread_created = false; + } +} + +void gsr_audio_capture_lock_filter(gsr_audio_capture *self) { + pthread_mutex_lock(&self->filter_mutex); +} + +void gsr_audio_capture_unlock_filter(gsr_audio_capture *self) { + pthread_mutex_unlock(&self->filter_mutex); +} + +static int audio_track_alloc_devices(gsr_audio_track *self, size_t num_devices) { + self->audio_devices = calloc(num_devices, sizeof(gsr_audio_device_capture)); + if(!self->audio_devices) { + gsr_log(GSR_LOG_LEVEL_ERROR, "failed to allocate audio devices"); + return GSR_ERROR_GENERIC; + } + return GSR_ERROR_OK; +} + +int gsr_audio_track_init_device_inputs(gsr_audio_track *self, const gsr_merged_audio_inputs *merged_audio_inputs, AVCodecContext *audio_codec_context, int num_channels, double num_audio_frames_shift, AVFilterContext **src_filter_ctx, bool use_amix) { + const int alloc_result = audio_track_alloc_devices(self, merged_audio_inputs->num_items); + if(alloc_result != GSR_ERROR_OK) + return alloc_result; + + for(size_t i = 0; i < merged_audio_inputs->num_items; ++i) { + const gsr_audio_input *audio_input = &merged_audio_inputs->items[i]; + gsr_audio_device_capture *device = &self->audio_devices[i]; + device->audio_input = *audio_input; + device->src_filter_ctx = use_amix ? src_filter_ctx[i] : NULL; + + if(audio_input->name[0] == '\0') { + device->sound_device.handle = NULL; + device->sound_device.frames = 0; + } else { + char description[GSR_AUDIO_INPUT_NAME_MAX_SIZE + 8]; + snprintf(description, sizeof(description), "gsr-%s", audio_input->name); + if(sound_device_get_by_name(&device->sound_device, description, audio_input->name, description, num_channels, audio_codec_context->frame_size, audio_codec_context_get_audio_format(audio_codec_context)) != 0) { + gsr_log(GSR_LOG_LEVEL_ERROR, "failed to get \"%s\" audio device", audio_input->name); + return GSR_ERROR_GENERIC; + } + } + + device->frame = create_audio_frame(audio_codec_context); + if(!device->frame) + return GSR_ERROR_GENERIC; + device->frame->pts = -audio_codec_context->frame_size * num_audio_frames_shift; + + ++self->num_audio_devices; + } + + return GSR_ERROR_OK; +} + +#ifdef GSR_APP_AUDIO +int gsr_audio_track_init_application_input(gsr_audio_track *self, const gsr_merged_audio_inputs *merged_audio_inputs, AVCodecContext *audio_codec_context, int num_channels, double num_audio_frames_shift, gsr_pipewire_audio *pipewire_audio) { + const int alloc_result = audio_track_alloc_devices(self, 1); + if(alloc_result != GSR_ERROR_OK) + return alloc_result; + + gsr_audio_device_capture *device = &self->audio_devices[0]; + device->frame = create_audio_frame(audio_codec_context); + if(!device->frame) + return GSR_ERROR_GENERIC; + device->frame->pts = -audio_codec_context->frame_size * num_audio_frames_shift; + ++self->num_audio_devices; + + char random_str[8]; + if(!generate_random_characters_standard_alphabet(random_str, sizeof(random_str))) { + gsr_log(GSR_LOG_LEVEL_ERROR, "failed to generate random string"); + return GSR_ERROR_GENERIC; + } + + char combined_sink_name[64]; + snprintf(combined_sink_name, sizeof(combined_sink_name), "gsr-combined-%.*s.monitor", (int)sizeof(random_str), random_str); + + if(sound_device_get_by_name(&device->sound_device, combined_sink_name, "", "gpu-screen-recorder", num_channels, audio_codec_context->frame_size, audio_codec_context_get_audio_format(audio_codec_context)) != 0) { + gsr_log(GSR_LOG_LEVEL_ERROR, "failed to setup audio recording to combined sink"); + return GSR_ERROR_GENERIC; + } + + const char **audio_devices_sources = calloc(merged_audio_inputs->num_items, sizeof(const char*)); + const char **app_names = calloc(merged_audio_inputs->num_items, sizeof(const char*)); + if(!audio_devices_sources || !app_names) { + free(audio_devices_sources); + free(app_names); + gsr_log(GSR_LOG_LEVEL_ERROR, "failed to allocate application audio names"); + return GSR_ERROR_GENERIC; + } + + size_t num_audio_devices_sources = 0; + size_t num_app_names = 0; + bool app_audio_inverted = false; + for(size_t i = 0; i < merged_audio_inputs->num_items; ++i) { + const gsr_audio_input *audio_input = &merged_audio_inputs->items[i]; + if(audio_input->type == GSR_AUDIO_INPUT_TYPE_DEVICE) { + audio_devices_sources[num_audio_devices_sources] = audio_input->name; + ++num_audio_devices_sources; + } else if(audio_input->type == GSR_AUDIO_INPUT_TYPE_APPLICATION) { + app_names[num_app_names] = audio_input->name; + ++num_app_names; + app_audio_inverted = audio_input->inverted; + } + } + + int result = GSR_ERROR_OK; + if(num_audio_devices_sources > 0) { + if(!gsr_pipewire_audio_add_link_from_sources_to_stream(pipewire_audio, audio_devices_sources, num_audio_devices_sources, combined_sink_name)) { + gsr_log(GSR_LOG_LEVEL_ERROR, "failed to add application audio link"); + result = GSR_ERROR_GENERIC; + } + } + + if(result == GSR_ERROR_OK) { + const bool link_added = app_audio_inverted + ? gsr_pipewire_audio_add_link_from_apps_to_stream_inverted(pipewire_audio, app_names, num_app_names, combined_sink_name) + : gsr_pipewire_audio_add_link_from_apps_to_stream(pipewire_audio, app_names, num_app_names, combined_sink_name); + if(!link_added) { + gsr_log(GSR_LOG_LEVEL_ERROR, "failed to add application audio link"); + result = GSR_ERROR_GENERIC; + } + } + + free(audio_devices_sources); + free(app_names); + return result; +} +#endif + +void gsr_audio_track_deinit(gsr_audio_track *self) { + for(size_t i = 0; i < self->num_audio_devices; ++i) { + gsr_audio_device_capture *device = &self->audio_devices[i]; + sound_device_close(&device->sound_device); + if(device->frame) + av_frame_free(&device->frame); + } + + if(self->audio_devices) { + free(self->audio_devices); + self->audio_devices = NULL; + } + self->num_audio_devices = 0; + + if(self->graph) + avfilter_graph_free(&self->graph); + + if(self->codec_context) + avcodec_free_context(&self->codec_context); +} + diff --git a/src/recorder/recording_clock.c b/src/recorder/recording_clock.c new file mode 100644 index 0000000..cb23d56 --- /dev/null +++ b/src/recorder/recording_clock.c @@ -0,0 +1,61 @@ +#include "../../include/recorder/recording_clock.h" +#include "../../include/utils.h" +#include "../../include/log.h" + +#include +#include +#include + +struct gsr_recording_clock { + double record_start_time; + _Atomic double paused_time_offset; + double paused_time_start; + atomic_bool paused; +}; + +gsr_recording_clock* gsr_recording_clock_create(void) { + gsr_recording_clock *self = calloc(1, sizeof(gsr_recording_clock)); + if(!self) { + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_recording_clock_create: failed to allocate recording clock"); + return NULL; + } + + atomic_init(&self->paused_time_offset, 0.0); + atomic_init(&self->paused, false); + self->record_start_time = clock_get_monotonic_seconds(); + return self; +} + +void gsr_recording_clock_destroy(gsr_recording_clock *self) { + if(self) + free(self); +} + +void gsr_recording_clock_start(gsr_recording_clock *self) { + self->record_start_time = clock_get_monotonic_seconds(); +} + +double gsr_recording_clock_get_start_time(const gsr_recording_clock *self) { + return self->record_start_time; +} + +double gsr_recording_clock_get_time(const gsr_recording_clock *self) { + return clock_get_monotonic_seconds() - atomic_load(&self->paused_time_offset); +} + +void gsr_recording_clock_set_paused(gsr_recording_clock *self, bool paused) { + if(paused == atomic_load(&self->paused)) + return; + + if(paused) { + self->paused_time_start = clock_get_monotonic_seconds(); + } else { + atomic_store(&self->paused_time_offset, atomic_load(&self->paused_time_offset) + (clock_get_monotonic_seconds() - self->paused_time_start)); + } + + atomic_store(&self->paused, paused); +} + +bool gsr_recording_clock_is_paused(const gsr_recording_clock *self) { + return atomic_load(&self->paused); +} -- cgit v1.2.3 From 40b6a13d14680cfacb3cdb59f04224540b80d9e4 Mon Sep 17 00:00:00 2001 From: dec05eba Date: Sat, 1 Aug 2026 23:02:07 +0200 Subject: Move muxing and replay saving from main.cpp into muxer and replay_save modules Replay saving uses a pthread with an owning result struct instead of std::future and a global output filepath. --- include/recorder/muxer.h | 40 ++++ include/recorder/replay_save.h | 53 ++++++ meson.build | 2 + src/main.cpp | 420 ++++------------------------------------- src/recorder/muxer.c | 268 ++++++++++++++++++++++++++ src/recorder/replay_save.c | 207 ++++++++++++++++++++ 6 files changed, 608 insertions(+), 382 deletions(-) create mode 100644 include/recorder/muxer.h create mode 100644 include/recorder/replay_save.h create mode 100644 src/recorder/muxer.c create mode 100644 src/recorder/replay_save.c diff --git a/include/recorder/muxer.h b/include/recorder/muxer.h new file mode 100644 index 0000000..e47cdde --- /dev/null +++ b/include/recorder/muxer.h @@ -0,0 +1,40 @@ +#ifndef GSR_RECORDER_MUXER_H +#define GSR_RECORDER_MUXER_H + +#include +#include +#include +#include "audio_capture.h" +#include "capture_setup.h" +#include "settings.h" + +#include + +typedef struct { + const gsr_audio_track *audio_track; + AVStream *stream; +} gsr_recording_audio_stream; + +typedef struct { + AVFormatContext *av_format_context; + AVStream *video_stream; + gsr_recording_audio_stream *audio_streams; + size_t num_audio_streams; +} gsr_recording_output; + +AVStream* create_stream(AVFormatContext *av_format_context, AVCodecContext *codec_context); +bool add_hdr_metadata_to_video_stream(gsr_capture *cap, AVStream *video_stream); +void set_format_context_options(AVFormatContext *av_format_context); +void av_write_header(AVFormatContext *av_format_context, const char *ffmpeg_opts); + +/* Returns false on failure. Creates the output file and its video and audio streams */ +bool gsr_recording_output_start(gsr_recording_output *self, const char *filename, const gsr_recorder_settings *settings, AVCodecContext *video_codec_context, const gsr_audio_capture *audio_capture, bool hdr, gsr_video_sources *video_sources); +bool gsr_recording_output_stop(gsr_recording_output *self); +gsr_recording_audio_stream* gsr_recording_output_get_audio_stream_by_index(gsr_recording_output *self, int stream_index); + +/* |filepath| should be at least PATH_MAX bytes in size. Creates the directories of the filepath */ +bool gsr_create_new_recording_filepath_from_timestamp(char *filepath, size_t filepath_size, const char *directory, const char *filename_prefix, const char *file_extension, bool date_folders); + +size_t calculate_estimated_replay_buffer_packets(int64_t replay_buffer_size_secs, int fps, gsr_audio_codec audio_codec, const gsr_audio_input_tracks *audio_inputs); + +#endif /* GSR_RECORDER_MUXER_H */ diff --git a/include/recorder/replay_save.h b/include/recorder/replay_save.h new file mode 100644 index 0000000..d92a8c0 --- /dev/null +++ b/include/recorder/replay_save.h @@ -0,0 +1,53 @@ +#ifndef GSR_RECORDER_REPLAY_SAVE_H +#define GSR_RECORDER_REPLAY_SAVE_H + +#include +#include +#include +#include +#include +#include "muxer.h" +#include "audio_capture.h" +#include "capture_setup.h" +#include "settings.h" + +#include "../encoder/encoder.h" +#include "../replay_buffer/replay_buffer.h" + +#define GSR_SAVE_REPLAY_SECONDS_FULL -1 + +typedef struct { + int64_t pts_offset; + int stream_index; +} gsr_audio_pts_offset; + +/* Saves the replay buffer to a file on a separate thread */ +typedef struct { + pthread_t thread; + bool thread_created; + volatile sig_atomic_t finished; + bool success; + char output_filepath[PATH_MAX]; + + AVCodecContext *video_codec_context; + int video_stream_index; + gsr_recording_output recording_output; + gsr_replay_buffer_iterator video_start_iterator; + int64_t video_pts_offset; + gsr_audio_pts_offset *audio_pts_offsets; + size_t num_audio_pts_offsets; + gsr_replay_buffer *cloned_replay_buffer; + gsr_encoder *encoder; +} gsr_replay_save; + +void gsr_replay_save_init(gsr_replay_save *self); +bool gsr_replay_save_is_running(const gsr_replay_save *self); + +/* Returns false if the replay failed to start. |current_save_replay_seconds| can be GSR_SAVE_REPLAY_SECONDS_FULL to save the whole replay buffer */ +bool gsr_replay_save_start(gsr_replay_save *self, AVCodecContext *video_codec_context, int video_stream_index, const gsr_audio_capture *audio_capture, gsr_encoder *encoder, const gsr_recorder_settings *settings, const char *file_extension, bool hdr, gsr_video_sources *video_sources, int current_save_replay_seconds); +/* Returns true when the replay finished saving, in which case |success| and |output_filepath| are set. |output_filepath| is empty when nothing was saved */ +bool gsr_replay_save_poll(gsr_replay_save *self, bool *success, const char **output_filepath); +/* Waits for an ongoing replay save to finish. Returns the same values as gsr_replay_save_poll */ +bool gsr_replay_save_join(gsr_replay_save *self, bool *success, const char **output_filepath); + +#endif /* GSR_RECORDER_REPLAY_SAVE_H */ diff --git a/meson.build b/meson.build index d56b21c..3b455a4 100644 --- a/meson.build +++ b/meson.build @@ -41,6 +41,8 @@ src = [ 'src/recorder/audio_input.c', 'src/recorder/audio_capture.c', 'src/recorder/recording_clock.c', + 'src/recorder/muxer.c', + 'src/recorder/replay_save.c', 'src/egl.c', 'src/cuda.c', 'src/window_texture.c', diff --git a/src/main.cpp b/src/main.cpp index b3056c3..6a030aa 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -8,6 +8,8 @@ extern "C" { #include "../include/recorder/capture_setup.h" #include "../include/recorder/audio_input.h" #include "../include/recorder/audio_capture.h" +#include "../include/recorder/muxer.h" +#include "../include/recorder/replay_save.h" #include "../include/recorder/error.h" #include "../include/capture/nvfbc.h" #include "../include/capture/xcomposite.h" @@ -89,8 +91,6 @@ static const int VIDEO_STREAM_INDEX = 0; /* AV1/VP9 use the 0-255 qindex quantizer scale and VP8 uses 0-127. Unlike h264 qp the quantizer step size is not exponential in qindex, so the mapping to an equivalent h264 qp is not linear in the quantizer range. qindex = h264 qp * 4 matches the qindex = crf * 4 scale used by libvpx/libaom, where crf is roughly equivalent to h264 qp. */ -static const int save_replay_seconds_full = -1; - static sig_atomic_t running = 1; static sig_atomic_t toggle_pause = 0; static sig_atomic_t toggle_replay_recording = 0; @@ -109,7 +109,7 @@ static void toggle_replay_recording_handler(int) { } static void save_replay_handler(int) { - save_replay_seconds = save_replay_seconds_full; + save_replay_seconds = GSR_SAVE_REPLAY_SECONDS_FULL; } static void save_replay_10_seconds_handler(int) { @@ -136,19 +136,6 @@ static void save_replay_30_minutes_handler(int) { save_replay_seconds = 60*30; } -static AVStream* create_stream(AVFormatContext *av_format_context, AVCodecContext *codec_context) { - AVStream *stream = avformat_new_stream(av_format_context, nullptr); - if (!stream) { - gsr_log(GSR_LOG_LEVEL_ERROR, "Could not allocate stream"); - _exit(1); - } - stream->id = av_format_context->nb_streams - 1; - stream->time_base = codec_context->time_base; - stream->avg_frame_rate = codec_context->framerate; - //stream->r_frame_rate = codec_context->framerate; - return stream; -} - static 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'; @@ -197,318 +184,6 @@ static void run_recording_saved_script_async(const char *script_file, const char } // TODO: Cleanup -static bool add_hdr_metadata_to_video_stream(gsr_capture *cap, AVStream *video_stream) { - size_t light_metadata_size = 0; - size_t mastering_display_metadata_size = 0; - AVContentLightMetadata *light_metadata = av_content_light_metadata_alloc(&light_metadata_size); - #if LIBAVUTIL_VERSION_INT < AV_VERSION_INT(59, 37, 100) - AVMasteringDisplayMetadata *mastering_display_metadata = av_mastering_display_metadata_alloc(); - mastering_display_metadata_size = sizeof(*mastering_display_metadata); - #else - AVMasteringDisplayMetadata *mastering_display_metadata = av_mastering_display_metadata_alloc_size(&mastering_display_metadata_size); - #endif - - if(!light_metadata || !mastering_display_metadata) { - if(light_metadata) - av_freep(&light_metadata); - - if(mastering_display_metadata) - av_freep(&mastering_display_metadata); - - return false; - } - - if(!gsr_capture_set_hdr_metadata(cap, mastering_display_metadata, light_metadata)) { - av_freep(&light_metadata); - av_freep(&mastering_display_metadata); - return false; - } - - // TODO: More error checking - - #if LIBAVCODEC_VERSION_INT < AV_VERSION_INT(60, 31, 102) - const bool content_light_level_added = av_stream_add_side_data(video_stream, AV_PKT_DATA_CONTENT_LIGHT_LEVEL, (uint8_t*)light_metadata, light_metadata_size) == 0; - #else - const bool content_light_level_added = av_packet_side_data_add(&video_stream->codecpar->coded_side_data, &video_stream->codecpar->nb_coded_side_data, AV_PKT_DATA_CONTENT_LIGHT_LEVEL, light_metadata, light_metadata_size, 0) != NULL; - #endif - - #if LIBAVCODEC_VERSION_INT < AV_VERSION_INT(60, 31, 102) - const bool mastering_display_metadata_added = av_stream_add_side_data(video_stream, AV_PKT_DATA_MASTERING_DISPLAY_METADATA, (uint8_t*)mastering_display_metadata, mastering_display_metadata_size) == 0; - #else - const bool mastering_display_metadata_added = av_packet_side_data_add(&video_stream->codecpar->coded_side_data, &video_stream->codecpar->nb_coded_side_data, AV_PKT_DATA_MASTERING_DISPLAY_METADATA, mastering_display_metadata, mastering_display_metadata_size, 0) != NULL; - #endif - - if(!content_light_level_added) - av_freep(&light_metadata); - - if(!mastering_display_metadata_added) - av_freep(&mastering_display_metadata); - - // Return true even on failure because we dont want to retry adding hdr metadata on failure - return true; -} - -static void set_format_context_options(AVFormatContext *av_format_context) { - if(LIBAVFORMAT_VERSION_INT >= AV_VERSION_INT(62, 6, 101)) { - av_opt_set(av_format_context->priv_data, "use_editlist", "1", 0); - const AVOption *opt = av_opt_find(av_format_context->priv_data, "movflags", NULL, 0, 0); - if (opt && opt->unit) { - const AVOption *flag = av_opt_find(av_format_context->priv_data, "hybrid_fragmented", opt->unit, 0, 0); - if (flag) - av_opt_set(av_format_context->priv_data, "movflags", "+hybrid_fragmented", 0); - } - } else { - const AVOutputFormat *output_format = av_format_context->oformat; - const char *file_extension = output_format->extensions ? output_format->extensions : ""; - if(strcmp(file_extension, "mp4") != 0 && strcmp(file_extension, "mov") != 0) - return; - - gsr_log(GSR_LOG_LEVEL_WARNING, "your FFmpeg version is known to be buggy (it doesn't have working hybrid_fragmented movflags). If you experience stutter in the mp4 file then update your FFmpeg to at least version 8 or record to a mkv file"); - } -} - -struct RecordingStartAudio { - const gsr_audio_track *audio_track; - AVStream *stream; -}; - -struct RecordingStartResult { - AVFormatContext *av_format_context = nullptr; - AVStream *video_stream = nullptr; - std::vector audio_inputs; -}; - -static RecordingStartResult start_recording_create_streams(const char *filename, const args_parser &arg_parser, AVCodecContext *video_codec_context, const gsr_audio_capture *audio_capture, bool hdr, gsr_video_sources *video_sources) { - AVFormatContext *av_format_context; - avformat_alloc_output_context2(&av_format_context, nullptr, arg_parser.settings.container_format, filename); - set_format_context_options(av_format_context); - - AVStream *video_stream = create_stream(av_format_context, video_codec_context); - avcodec_parameters_from_context(video_stream->codecpar, video_codec_context); - - RecordingStartResult result; - result.audio_inputs.reserve(audio_capture->num_tracks); - - for(size_t audio_track_index = 0; audio_track_index < audio_capture->num_tracks; ++audio_track_index) { - const gsr_audio_track &audio_track = audio_capture->tracks[audio_track_index]; - AVStream *audio_stream = create_stream(av_format_context, audio_track.codec_context); - if(!audio_track.name[0] == '\0' && !arg_parser.settings.exclude_metadata) - av_dict_set(&audio_stream->metadata, "title", audio_track.name, 0); - avcodec_parameters_from_context(audio_stream->codecpar, audio_track.codec_context); - result.audio_inputs.push_back({&audio_track, audio_stream}); - } - - const int open_ret = avio_open(&av_format_context->pb, filename, AVIO_FLAG_WRITE); - if(open_ret < 0) { - gsr_log(GSR_LOG_LEVEL_ERROR, "start_recording_create_streams: could not open '%s': %s", filename, gsr_av_error_to_string(open_ret)); - return result; - } - - AVDictionary *options = nullptr; - av_dict_set(&options, "strict", "experimental", 0); - - if(arg_parser.settings.ffmpeg_opts) - av_dict_parse_string(&options, arg_parser.settings.ffmpeg_opts, "=", ";", 0); - - const int header_write_ret = avformat_write_header(av_format_context, &options); - av_dict_free(&options); - if(header_write_ret < 0) { - gsr_log(GSR_LOG_LEVEL_ERROR, "start_recording_create_streams: error occurred when writing header to output file: %s", gsr_av_error_to_string(header_write_ret)); - avio_close(av_format_context->pb); - avformat_free_context(av_format_context); - return result; - } - - for(size_t video_source_index = 0; video_source_index < video_sources->num_items; ++video_source_index) { - gsr_video_source &video_source = video_sources->items[video_source_index]; - if(hdr && add_hdr_metadata_to_video_stream(video_source.capture, video_stream)) - break; - } - - result.av_format_context = av_format_context; - result.video_stream = video_stream; - return result; -} - -static bool stop_recording_close_streams(AVFormatContext *av_format_context) { - bool trailer_written = true; - if(av_write_trailer(av_format_context) != 0) { - //fprintf(stderr, "gsr error: end: failed to write trailer\n"); - //trailer_written = false; - } - - const bool closed = avio_close(av_format_context->pb) == 0; - avformat_free_context(av_format_context); - return trailer_written && closed; -} - -static std::future save_replay_thread; -static std::string save_replay_output_filepath; - -static std::string create_new_recording_filepath_from_timestamp(std::string directory, const char *filename_prefix, const std::string &file_extension, bool date_folders) { - char date_str[128]; - std::string output_filepath; - if(date_folders) { - gsr_get_date_only_str(date_str, sizeof(date_str)); - std::string output_folder = directory + '/' + date_str; - if(create_directory_recursive(&output_folder[0]) != 0) - gsr_log(GSR_LOG_LEVEL_ERROR, "failed to create directory: %s", output_folder.c_str()); - gsr_get_time_only_str(date_str, sizeof(date_str)); - output_filepath = output_folder + "/" + filename_prefix + "_" + date_str + "." + file_extension; - } else { - if(create_directory_recursive(&directory[0]) != 0) - gsr_log(GSR_LOG_LEVEL_ERROR, "failed to create directory: %s", directory.c_str()); - gsr_get_date_str(date_str, sizeof(date_str)); - output_filepath = directory + "/" + filename_prefix + "_" + date_str + "." + file_extension; - } - return output_filepath; -} - -static RecordingStartAudio* get_recording_start_item_by_stream_index(RecordingStartResult &result, int stream_index) { - for(auto &audio_input : result.audio_inputs) { - if(audio_input.stream->index == stream_index) - return &audio_input; - } - return nullptr; -} - -struct AudioPtsOffset { - int64_t pts_offset = 0; - int stream_index = 0; -}; - -static bool save_replay_async(AVCodecContext *video_codec_context, int video_stream_index, const gsr_audio_capture *audio_capture, gsr_encoder *encoder, const args_parser &arg_parser, const std::string &file_extension, bool date_folders, bool hdr, gsr_video_sources *video_sources, int current_save_replay_seconds) { - if(save_replay_thread.valid()) - return true; - - pthread_mutex_lock(&encoder->replay_mutex); - gsr_replay_buffer *cloned_replay_buffer = gsr_replay_buffer_clone(encoder->replay_buffer); - pthread_mutex_unlock(&encoder->replay_mutex); - if(!cloned_replay_buffer) { - // TODO: Return this error to mark the replay as failed - gsr_log(GSR_LOG_LEVEL_ERROR, "failed to save replay: failed to clone replay buffer"); - return false; - } - - const gsr_replay_buffer_iterator search_start_iterator = current_save_replay_seconds == save_replay_seconds_full ? gsr_replay_buffer_iterator{0, 0} : gsr_replay_buffer_find_packet_index_by_time_passed(cloned_replay_buffer, current_save_replay_seconds); - const gsr_replay_buffer_iterator video_start_iterator = gsr_replay_buffer_find_keyframe(cloned_replay_buffer, search_start_iterator, video_stream_index, false); - if(video_start_iterator.packet_index == (size_t)-1) { - gsr_log(GSR_LOG_LEVEL_ERROR, "failed to save replay: failed to find a video keyframe. perhaps replay was saved too fast, before anything has been recorded"); - pthread_mutex_lock(&encoder->replay_mutex); - gsr_replay_buffer_destroy(cloned_replay_buffer); - pthread_mutex_unlock(&encoder->replay_mutex); - return true; - } - - const int64_t video_pts_offset = gsr_replay_buffer_iterator_get_packet(cloned_replay_buffer, video_start_iterator)->pts; - - std::vector audio_pts_offsets; - audio_pts_offsets.reserve(audio_capture->num_tracks); - for(size_t audio_track_index = 0; audio_track_index < audio_capture->num_tracks; ++audio_track_index) { - const gsr_audio_track &audio_track = audio_capture->tracks[audio_track_index]; - const gsr_replay_buffer_iterator audio_start_iterator = gsr_replay_buffer_find_keyframe(cloned_replay_buffer, video_start_iterator, audio_track.stream_index, false); - const int64_t audio_pts_offset = audio_start_iterator.packet_index == (size_t)-1 ? 0 : gsr_replay_buffer_iterator_get_packet(cloned_replay_buffer, audio_start_iterator)->pts; - audio_pts_offsets.push_back(AudioPtsOffset{audio_pts_offset, audio_track.stream_index}); - } - - std::string output_filepath = create_new_recording_filepath_from_timestamp(arg_parser.settings.filename, "Replay", file_extension, date_folders); - RecordingStartResult recording_start_result = start_recording_create_streams(output_filepath.c_str(), arg_parser, video_codec_context, audio_capture, hdr, video_sources); - if(!recording_start_result.av_format_context) { - pthread_mutex_lock(&encoder->replay_mutex); - gsr_replay_buffer_destroy(cloned_replay_buffer); - pthread_mutex_unlock(&encoder->replay_mutex); - return false; - } - - save_replay_output_filepath = std::move(output_filepath); - - save_replay_thread = std::async(std::launch::async, [video_stream_index, recording_start_result, video_start_iterator, video_pts_offset, audio_pts_offsets{std::move(audio_pts_offsets)}, video_codec_context, cloned_replay_buffer, encoder]() mutable { - bool success = true; - gsr_replay_buffer_iterator replay_iterator = video_start_iterator; - - for(;;) { - AVPacket *replay_packet = gsr_replay_buffer_iterator_get_packet(cloned_replay_buffer, replay_iterator); - uint8_t *replay_packet_data = NULL; - if(replay_packet) { - pthread_mutex_lock(&encoder->replay_mutex); - replay_packet_data = gsr_replay_buffer_iterator_get_packet_data(cloned_replay_buffer, replay_iterator); - pthread_mutex_unlock(&encoder->replay_mutex); - } - - if(!replay_packet) { - gsr_log(GSR_LOG_LEVEL_ERROR, "save_replay_async: no replay packet"); - success = false; - break; - } - - if(!replay_packet->data && !replay_packet_data) { - gsr_log(GSR_LOG_LEVEL_ERROR, "save_replay_async: no replay packet data"); - success = false; - break; - } - - // TODO: Check if successful - AVPacket av_packet; - memset(&av_packet, 0, sizeof(av_packet)); - //av_packet_from_data(av_packet, replay_packet->data, replay_packet->size); - av_packet.data = replay_packet->data ? replay_packet->data : replay_packet_data; - av_packet.size = replay_packet->size; - av_packet.stream_index = replay_packet->stream_index; - av_packet.pts = replay_packet->pts; - av_packet.dts = replay_packet->pts; - av_packet.flags = replay_packet->flags; - //av_packet.duration = replay_packet->duration; - - AVStream *stream = recording_start_result.video_stream; - AVCodecContext *codec_context = video_codec_context; - - if(av_packet.stream_index == video_stream_index) { - av_packet.pts -= video_pts_offset; - av_packet.dts -= video_pts_offset; - } else { - RecordingStartAudio *recording_start_audio = get_recording_start_item_by_stream_index(recording_start_result, av_packet.stream_index); - if(!recording_start_audio) { - gsr_log(GSR_LOG_LEVEL_ERROR, "save_replay_async: failed to find audio stream by index: %d", av_packet.stream_index); - free(replay_packet_data); - continue; - } - - const gsr_audio_track *audio_track = recording_start_audio->audio_track; - stream = recording_start_audio->stream; - codec_context = audio_track->codec_context; - - const AudioPtsOffset &audio_pts_offset = audio_pts_offsets[av_packet.stream_index - 1]; - assert(audio_pts_offset.stream_index == av_packet.stream_index); - av_packet.pts -= audio_pts_offset.pts_offset; - av_packet.dts -= audio_pts_offset.pts_offset; - } - - //av_packet.stream_index = stream->index; - av_packet_rescale_ts(&av_packet, codec_context->time_base, stream->time_base); - - const int ret = av_write_frame(recording_start_result.av_format_context, &av_packet); - if(ret < 0) - gsr_log(GSR_LOG_LEVEL_ERROR, "Failed to write frame index %d to muxer, reason: %s (%d)", av_packet.stream_index, gsr_av_error_to_string(ret), ret); - - free(replay_packet_data); - - //av_packet_free(&av_packet); - if(!gsr_replay_buffer_iterator_next(cloned_replay_buffer, &replay_iterator)) - break; - } - - stop_recording_close_streams(recording_start_result.av_format_context); - - pthread_mutex_lock(&encoder->replay_mutex); - gsr_replay_buffer_destroy(cloned_replay_buffer); - pthread_mutex_unlock(&encoder->replay_mutex); - - return success; - }); - - return true; -} - static void list_system_info(bool wayland) { printf("display_server|%s\n", wayland ? "wayland" : "x11"); bool supports_app_audio = false; @@ -1014,31 +689,6 @@ static bool get_image_format_from_filename(const char *filename, gsr_image_forma } } -static void av_write_header(AVFormatContext *av_format_context, const char *ffmpeg_opts) { - AVDictionary *options = nullptr; - av_dict_set(&options, "strict", "experimental", 0); - - if(ffmpeg_opts) - av_dict_parse_string(&options, ffmpeg_opts, "=", ";", 0); - - const int ret = avformat_write_header(av_format_context, &options); - if(ret < 0) - fprintf(stderr, "Error occurred when writing header to output file: %s\n", gsr_av_error_to_string(ret)); - - av_dict_free(&options); -} - -static size_t calculate_estimated_replay_buffer_packets(int64_t replay_buffer_size_secs, int fps, gsr_audio_codec audio_codec, const gsr_audio_input_tracks *audio_inputs) { - if(replay_buffer_size_secs == -1) - return 0; - - int audio_fps = 0; - if(audio_inputs->num_items > 0) - audio_fps = GSR_AUDIO_SAMPLE_RATE / audio_codec_get_frame_size(audio_codec); - - return replay_buffer_size_secs * (fps + audio_fps * audio_inputs->num_items); -} - static void set_display_server_environment_variables() { // Some users dont have properly setup environments (no display manager that does systemctl --user import-environment DISPLAY WAYLAND_DISPLAY) const char *display = getenv("DISPLAY"); @@ -1584,7 +1234,10 @@ int main(int argc, char **argv) { bool paused = false; bool replay_recording = false; - RecordingStartResult replay_recording_start_result; + gsr_recording_output replay_recording_output; + memset(&replay_recording_output, 0, sizeof(replay_recording_output)); + gsr_replay_save replay_save; + gsr_replay_save_init(&replay_save); std::vector replay_recording_items; std::string replay_recording_filepath; bool force_iframe_frame = false; // Only needed for video since audio frames are always iframes @@ -1828,10 +1481,11 @@ int main(int argc, char **argv) { if(new_replay_recording_state) { gsr_audio_capture_lock_filter(&audio_capture); replay_recording_items.clear(); - replay_recording_filepath = create_new_recording_filepath_from_timestamp(arg_parser.settings.replay_recording_directory, "Video", file_extension, arg_parser.settings.date_folders); - replay_recording_start_result = start_recording_create_streams(replay_recording_filepath.c_str(), arg_parser, video_codec_context, &audio_capture, hdr, video_sources); - if(replay_recording_start_result.av_format_context) { - const size_t video_recording_destination_id = gsr_encoder_add_recording_destination(&encoder, video_codec_context, replay_recording_start_result.av_format_context, replay_recording_start_result.video_stream, video_frame->pts); + char replay_recording_filepath_buf[PATH_MAX]; + const bool filepath_created = gsr_create_new_recording_filepath_from_timestamp(replay_recording_filepath_buf, sizeof(replay_recording_filepath_buf), arg_parser.settings.replay_recording_directory, "Video", file_extension.c_str(), arg_parser.settings.date_folders); + replay_recording_filepath = filepath_created ? replay_recording_filepath_buf : ""; + if(filepath_created && gsr_recording_output_start(&replay_recording_output, replay_recording_filepath.c_str(), &arg_parser.settings, video_codec_context, &audio_capture, hdr, video_sources)) { + const size_t video_recording_destination_id = gsr_encoder_add_recording_destination(&encoder, video_codec_context, replay_recording_output.av_format_context, replay_recording_output.video_stream, video_frame->pts); if(arg_parser.settings.write_first_frame_ts && video_recording_destination_id != (size_t)-1) { std::string ts_filepath = replay_recording_filepath + ".ts"; gsr_encoder_set_recording_destination_first_frame_ts_filepath(&encoder, video_recording_destination_id, ts_filepath.c_str()); @@ -1840,27 +1494,29 @@ int main(int argc, char **argv) { if(video_recording_destination_id != (size_t)-1) replay_recording_items.push_back(video_recording_destination_id); - for(const auto &audio_input : replay_recording_start_result.audio_inputs) { - const size_t audio_recording_destination_id = gsr_encoder_add_recording_destination(&encoder, audio_input.audio_track->codec_context, replay_recording_start_result.av_format_context, audio_input.stream, audio_input.audio_track->pts); + for(size_t i = 0; i < replay_recording_output.num_audio_streams; ++i) { + const gsr_recording_audio_stream *audio_stream = &replay_recording_output.audio_streams[i]; + const size_t audio_recording_destination_id = gsr_encoder_add_recording_destination(&encoder, audio_stream->audio_track->codec_context, replay_recording_output.av_format_context, audio_stream->stream, audio_stream->audio_track->pts); if(audio_recording_destination_id != (size_t)-1) replay_recording_items.push_back(audio_recording_destination_id); } replay_recording = true; force_iframe_frame = true; - fprintf(stderr, "Started recording\n"); + gsr_log(GSR_LOG_LEVEL_INFO, "Started recording"); } else { printf("gsr error: Failed to start recording\n"); fflush(stdout); } - } else if(replay_recording_start_result.av_format_context) { + gsr_audio_capture_unlock_filter(&audio_capture); + } else if(replay_recording_output.av_format_context) { for(size_t id : replay_recording_items) { gsr_encoder_remove_recording_destination(&encoder, id); } replay_recording_items.clear(); - if(stop_recording_close_streams(replay_recording_start_result.av_format_context)) { - fprintf(stderr, "Stopped recording\n"); + if(gsr_recording_output_stop(&replay_recording_output)) { + gsr_log(GSR_LOG_LEVEL_INFO, "Stopped recording"); puts(replay_recording_filepath.c_str()); fflush(stdout); if(arg_parser.settings.recording_saved_script) @@ -1870,39 +1526,38 @@ int main(int argc, char **argv) { fflush(stdout); } - replay_recording_start_result = RecordingStartResult{}; replay_recording = false; replay_recording_filepath.clear(); } } - if(save_replay_thread.valid() && save_replay_thread.wait_for(std::chrono::seconds(0)) == std::future_status::ready) { - const bool replay_save_result = save_replay_thread.get(); - if(save_replay_output_filepath.empty() || !replay_save_result) { + bool replay_save_result = false; + const char *replay_save_output_filepath = nullptr; + if(gsr_replay_save_poll(&replay_save, &replay_save_result, &replay_save_output_filepath)) { + if(replay_save_output_filepath[0] == '\0' || !replay_save_result) { printf("gsr error: Failed to save replay\n"); fflush(stdout); } else { - puts(save_replay_output_filepath.c_str()); + puts(replay_save_output_filepath); fflush(stdout); if(arg_parser.settings.recording_saved_script) - run_recording_saved_script_async(arg_parser.settings.recording_saved_script, save_replay_output_filepath.c_str(), "replay"); + run_recording_saved_script_async(arg_parser.settings.recording_saved_script, replay_save_output_filepath, "replay"); } } - if(save_replay_seconds != 0 && !save_replay_thread.valid() && arg_parser.settings.is_replaying) { + if(save_replay_seconds != 0 && !gsr_replay_save_is_running(&replay_save) && arg_parser.settings.is_replaying) { int current_save_replay_seconds = save_replay_seconds; if(current_save_replay_seconds > 0) current_save_replay_seconds += arg_parser.settings.keyint; save_replay_seconds = 0; - save_replay_output_filepath.clear(); - const bool replay_start_result = save_replay_async(video_codec_context, VIDEO_STREAM_INDEX, &audio_capture, &encoder, arg_parser, file_extension, arg_parser.settings.date_folders, hdr, video_sources, current_save_replay_seconds); + const bool replay_start_result = gsr_replay_save_start(&replay_save, video_codec_context, VIDEO_STREAM_INDEX, &audio_capture, &encoder, &arg_parser.settings, file_extension.c_str(), hdr, video_sources, current_save_replay_seconds); if(!replay_start_result) { printf("gsr error: Failed to save replay\n"); fflush(stdout); } - if(arg_parser.settings.restart_replay_on_save && current_save_replay_seconds == save_replay_seconds_full) { + if(arg_parser.settings.restart_replay_on_save && current_save_replay_seconds == GSR_SAVE_REPLAY_SECONDS_FULL) { pthread_mutex_lock(&encoder.replay_mutex); gsr_replay_buffer_clear(encoder.replay_buffer); pthread_mutex_unlock(&encoder.replay_mutex); @@ -1930,28 +1585,29 @@ int main(int argc, char **argv) { running = 0; - if(save_replay_thread.valid()) { - save_replay_thread.get(); - if(save_replay_output_filepath.empty()) { + bool final_replay_save_result = false; + const char *final_replay_save_output_filepath = nullptr; + if(gsr_replay_save_join(&replay_save, &final_replay_save_result, &final_replay_save_output_filepath)) { + if(final_replay_save_output_filepath[0] == '\0') { // TODO: Output failed to save } else { - puts(save_replay_output_filepath.c_str()); + puts(final_replay_save_output_filepath); fflush(stdout); if(arg_parser.settings.recording_saved_script) - run_recording_saved_script_async(arg_parser.settings.recording_saved_script, save_replay_output_filepath.c_str(), "replay"); + run_recording_saved_script_async(arg_parser.settings.recording_saved_script, final_replay_save_output_filepath, "replay"); } } gsr_plugins_deinit(&plugins); - if(replay_recording_start_result.av_format_context) { + if(replay_recording_output.av_format_context) { for(size_t id : replay_recording_items) { gsr_encoder_remove_recording_destination(&encoder, id); } replay_recording_items.clear(); - if(stop_recording_close_streams(replay_recording_start_result.av_format_context)) { - fprintf(stderr, "Stopped recording\n"); + if(gsr_recording_output_stop(&replay_recording_output)) { + gsr_log(GSR_LOG_LEVEL_INFO, "Stopped recording"); puts(replay_recording_filepath.c_str()); fflush(stdout); if(arg_parser.settings.recording_saved_script) diff --git a/src/recorder/muxer.c b/src/recorder/muxer.c new file mode 100644 index 0000000..ff4ec06 --- /dev/null +++ b/src/recorder/muxer.c @@ -0,0 +1,268 @@ +#include "../../include/recorder/muxer.h" +#include "../../include/recorder/audio_codec.h" +#include "../../include/ffmpeg_utils.h" +#include "../../include/utils.h" +#include "../../include/log.h" + +#include +#include +#include + +#include +#include + +static void gsr_recording_output_deinit(gsr_recording_output *self) { + if(self->audio_streams) { + free(self->audio_streams); + self->audio_streams = NULL; + } + self->num_audio_streams = 0; +} + +AVStream* create_stream(AVFormatContext *av_format_context, AVCodecContext *codec_context) { + AVStream *stream = avformat_new_stream(av_format_context, NULL); + if (!stream) { + gsr_log(GSR_LOG_LEVEL_ERROR, "Could not allocate stream"); + return NULL; + } + stream->id = av_format_context->nb_streams - 1; + stream->time_base = codec_context->time_base; + stream->avg_frame_rate = codec_context->framerate; + //stream->r_frame_rate = codec_context->framerate; + return stream; +} + +bool add_hdr_metadata_to_video_stream(gsr_capture *cap, AVStream *video_stream) { + size_t light_metadata_size = 0; + size_t mastering_display_metadata_size = 0; + AVContentLightMetadata *light_metadata = av_content_light_metadata_alloc(&light_metadata_size); + #if LIBAVUTIL_VERSION_INT < AV_VERSION_INT(59, 37, 100) + AVMasteringDisplayMetadata *mastering_display_metadata = av_mastering_display_metadata_alloc(); + mastering_display_metadata_size = sizeof(*mastering_display_metadata); + #else + AVMasteringDisplayMetadata *mastering_display_metadata = av_mastering_display_metadata_alloc_size(&mastering_display_metadata_size); + #endif + + if(!light_metadata || !mastering_display_metadata) { + if(light_metadata) + av_freep(&light_metadata); + + if(mastering_display_metadata) + av_freep(&mastering_display_metadata); + + return false; + } + + if(!gsr_capture_set_hdr_metadata(cap, mastering_display_metadata, light_metadata)) { + av_freep(&light_metadata); + av_freep(&mastering_display_metadata); + return false; + } + + // TODO: More error checking + + #if LIBAVCODEC_VERSION_INT < AV_VERSION_INT(60, 31, 102) + const bool content_light_level_added = av_stream_add_side_data(video_stream, AV_PKT_DATA_CONTENT_LIGHT_LEVEL, (uint8_t*)light_metadata, light_metadata_size) == 0; + #else + const bool content_light_level_added = av_packet_side_data_add(&video_stream->codecpar->coded_side_data, &video_stream->codecpar->nb_coded_side_data, AV_PKT_DATA_CONTENT_LIGHT_LEVEL, light_metadata, light_metadata_size, 0) != NULL; + #endif + + #if LIBAVCODEC_VERSION_INT < AV_VERSION_INT(60, 31, 102) + const bool mastering_display_metadata_added = av_stream_add_side_data(video_stream, AV_PKT_DATA_MASTERING_DISPLAY_METADATA, (uint8_t*)mastering_display_metadata, mastering_display_metadata_size) == 0; + #else + const bool mastering_display_metadata_added = av_packet_side_data_add(&video_stream->codecpar->coded_side_data, &video_stream->codecpar->nb_coded_side_data, AV_PKT_DATA_MASTERING_DISPLAY_METADATA, mastering_display_metadata, mastering_display_metadata_size, 0) != NULL; + #endif + + if(!content_light_level_added) + av_freep(&light_metadata); + + if(!mastering_display_metadata_added) + av_freep(&mastering_display_metadata); + + // Return true even on failure because we dont want to retry adding hdr metadata on failure + return true; +} + +void set_format_context_options(AVFormatContext *av_format_context) { + if(LIBAVFORMAT_VERSION_INT >= AV_VERSION_INT(62, 6, 101)) { + av_opt_set(av_format_context->priv_data, "use_editlist", "1", 0); + const AVOption *opt = av_opt_find(av_format_context->priv_data, "movflags", NULL, 0, 0); + if (opt && opt->unit) { + const AVOption *flag = av_opt_find(av_format_context->priv_data, "hybrid_fragmented", opt->unit, 0, 0); + if (flag) + av_opt_set(av_format_context->priv_data, "movflags", "+hybrid_fragmented", 0); + } + } else { + const AVOutputFormat *output_format = av_format_context->oformat; + const char *file_extension = output_format->extensions ? output_format->extensions : ""; + if(strcmp(file_extension, "mp4") != 0 && strcmp(file_extension, "mov") != 0) + return; + + gsr_log(GSR_LOG_LEVEL_WARNING, "your FFmpeg version is known to be buggy (it doesn't have working hybrid_fragmented movflags). If you experience stutter in the mp4 file then update your FFmpeg to at least version 8 or record to a mkv file"); + } +} + +void av_write_header(AVFormatContext *av_format_context, const char *ffmpeg_opts) { + AVDictionary *options = NULL; + av_dict_set(&options, "strict", "experimental", 0); + + if(ffmpeg_opts) + av_dict_parse_string(&options, ffmpeg_opts, "=", ";", 0); + + const int ret = avformat_write_header(av_format_context, &options); + if(ret < 0) + gsr_log(GSR_LOG_LEVEL_ERROR, "error occurred when writing header to output file: %s", gsr_av_error_to_string(ret)); + + av_dict_free(&options); +} + +bool gsr_recording_output_start(gsr_recording_output *self, const char *filename, const gsr_recorder_settings *settings, AVCodecContext *video_codec_context, const gsr_audio_capture *audio_capture, bool hdr, gsr_video_sources *video_sources) { + memset(self, 0, sizeof(*self)); + + AVFormatContext *av_format_context = NULL; + avformat_alloc_output_context2(&av_format_context, NULL, settings->container_format, filename); + if(!av_format_context) { + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_recording_output_start: failed to create output context for '%s'", filename); + return false; + } + set_format_context_options(av_format_context); + + AVStream *video_stream = create_stream(av_format_context, video_codec_context); + if(!video_stream) { + avformat_free_context(av_format_context); + return false; + } + avcodec_parameters_from_context(video_stream->codecpar, video_codec_context); + + if(audio_capture->num_tracks > 0) { + self->audio_streams = calloc(audio_capture->num_tracks, sizeof(gsr_recording_audio_stream)); + if(!self->audio_streams) { + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_recording_output_start: failed to allocate audio streams"); + avformat_free_context(av_format_context); + return false; + } + } + + for(size_t i = 0; i < audio_capture->num_tracks; ++i) { + const gsr_audio_track *audio_track = &audio_capture->tracks[i]; + AVStream *audio_stream = create_stream(av_format_context, audio_track->codec_context); + if(!audio_stream) { + gsr_recording_output_deinit(self); + avformat_free_context(av_format_context); + return false; + } + + if(audio_track->name[0] != '\0' && !settings->exclude_metadata) + av_dict_set(&audio_stream->metadata, "title", audio_track->name, 0); + avcodec_parameters_from_context(audio_stream->codecpar, audio_track->codec_context); + + self->audio_streams[i].audio_track = audio_track; + self->audio_streams[i].stream = audio_stream; + ++self->num_audio_streams; + } + + const int open_ret = avio_open(&av_format_context->pb, filename, AVIO_FLAG_WRITE); + if(open_ret < 0) { + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_recording_output_start: could not open '%s': %s", filename, gsr_av_error_to_string(open_ret)); + gsr_recording_output_deinit(self); + avformat_free_context(av_format_context); + return false; + } + + AVDictionary *options = NULL; + av_dict_set(&options, "strict", "experimental", 0); + + if(settings->ffmpeg_opts) + av_dict_parse_string(&options, settings->ffmpeg_opts, "=", ";", 0); + + const int header_write_ret = avformat_write_header(av_format_context, &options); + av_dict_free(&options); + if(header_write_ret < 0) { + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_recording_output_start: error occurred when writing header to output file: %s", gsr_av_error_to_string(header_write_ret)); + avio_close(av_format_context->pb); + gsr_recording_output_deinit(self); + avformat_free_context(av_format_context); + return false; + } + + for(size_t i = 0; i < video_sources->num_items; ++i) { + if(hdr && add_hdr_metadata_to_video_stream(video_sources->items[i].capture, video_stream)) + break; + } + + self->av_format_context = av_format_context; + self->video_stream = video_stream; + return true; +} + +bool gsr_recording_output_stop(gsr_recording_output *self) { + bool trailer_written = true; + if(av_write_trailer(self->av_format_context) != 0) { + //trailer_written = false; + } + + const bool closed = avio_close(self->av_format_context->pb) == 0; + avformat_free_context(self->av_format_context); + self->av_format_context = NULL; + self->video_stream = NULL; + gsr_recording_output_deinit(self); + return trailer_written && closed; +} + +bool gsr_create_new_recording_filepath_from_timestamp(char *filepath, size_t filepath_size, const char *directory, const char *filename_prefix, const char *file_extension, bool date_folders) { + char date_str[128]; + char output_folder[PATH_MAX]; + int written = 0; + + if(date_folders) { + gsr_get_date_only_str(date_str, sizeof(date_str)); + written = snprintf(output_folder, sizeof(output_folder), "%s/%s", directory, date_str); + if(written < 0 || written >= (int)sizeof(output_folder)) { + gsr_log(GSR_LOG_LEVEL_ERROR, "the directory path is too long: %s", directory); + return false; + } + + if(create_directory_recursive(output_folder) != 0) + gsr_log(GSR_LOG_LEVEL_ERROR, "failed to create directory: %s", output_folder); + + gsr_get_time_only_str(date_str, sizeof(date_str)); + } else { + written = snprintf(output_folder, sizeof(output_folder), "%s", directory); + if(written < 0 || written >= (int)sizeof(output_folder)) { + gsr_log(GSR_LOG_LEVEL_ERROR, "the directory path is too long: %s", directory); + return false; + } + + if(create_directory_recursive(output_folder) != 0) + gsr_log(GSR_LOG_LEVEL_ERROR, "failed to create directory: %s", output_folder); + + gsr_get_date_str(date_str, sizeof(date_str)); + } + + written = snprintf(filepath, filepath_size, "%s/%s_%s.%s", output_folder, filename_prefix, date_str, file_extension); + if(written < 0 || written >= (int)filepath_size) { + gsr_log(GSR_LOG_LEVEL_ERROR, "the output filepath is too long"); + return false; + } + + return true; +} + +gsr_recording_audio_stream* gsr_recording_output_get_audio_stream_by_index(gsr_recording_output *self, int stream_index) { + for(size_t i = 0; i < self->num_audio_streams; ++i) { + if(self->audio_streams[i].stream->index == stream_index) + return &self->audio_streams[i]; + } + return NULL; +} + +size_t calculate_estimated_replay_buffer_packets(int64_t replay_buffer_size_secs, int fps, gsr_audio_codec audio_codec, const gsr_audio_input_tracks *audio_inputs) { + if(replay_buffer_size_secs == -1) + return 0; + + int audio_fps = 0; + if(audio_inputs->num_items > 0) + audio_fps = GSR_AUDIO_SAMPLE_RATE / audio_codec_get_frame_size(audio_codec); + + return replay_buffer_size_secs * (fps + audio_fps * audio_inputs->num_items); +} diff --git a/src/recorder/replay_save.c b/src/recorder/replay_save.c new file mode 100644 index 0000000..6e5bc7b --- /dev/null +++ b/src/recorder/replay_save.c @@ -0,0 +1,207 @@ +#include "../../include/recorder/replay_save.h" +#include "../../include/ffmpeg_utils.h" +#include "../../include/log.h" + +#include +#include +#include + +void gsr_replay_save_init(gsr_replay_save *self) { + memset(self, 0, sizeof(*self)); + self->finished = 0; +} + +bool gsr_replay_save_is_running(const gsr_replay_save *self) { + return self->thread_created; +} + +static void gsr_replay_save_cleanup(gsr_replay_save *self) { + if(self->cloned_replay_buffer) { + pthread_mutex_lock(&self->encoder->replay_mutex); + gsr_replay_buffer_destroy(self->cloned_replay_buffer); + pthread_mutex_unlock(&self->encoder->replay_mutex); + self->cloned_replay_buffer = NULL; + } + + if(self->audio_pts_offsets) { + free(self->audio_pts_offsets); + self->audio_pts_offsets = NULL; + } + self->num_audio_pts_offsets = 0; +} + +static void* replay_save_thread(void *userdata) { + gsr_replay_save *self = userdata; + bool success = true; + gsr_replay_buffer_iterator replay_iterator = self->video_start_iterator; + + for(;;) { + AVPacket *replay_packet = gsr_replay_buffer_iterator_get_packet(self->cloned_replay_buffer, replay_iterator); + uint8_t *replay_packet_data = NULL; + if(replay_packet) { + pthread_mutex_lock(&self->encoder->replay_mutex); + replay_packet_data = gsr_replay_buffer_iterator_get_packet_data(self->cloned_replay_buffer, replay_iterator); + pthread_mutex_unlock(&self->encoder->replay_mutex); + } + + if(!replay_packet) { + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_replay_save: no replay packet"); + success = false; + break; + } + + if(!replay_packet->data && !replay_packet_data) { + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_replay_save: no replay packet data"); + success = false; + break; + } + + // TODO: Check if successful + AVPacket av_packet; + memset(&av_packet, 0, sizeof(av_packet)); + //av_packet_from_data(av_packet, replay_packet->data, replay_packet->size); + av_packet.data = replay_packet->data ? replay_packet->data : replay_packet_data; + av_packet.size = replay_packet->size; + av_packet.stream_index = replay_packet->stream_index; + av_packet.pts = replay_packet->pts; + av_packet.dts = replay_packet->pts; + av_packet.flags = replay_packet->flags; + //av_packet.duration = replay_packet->duration; + + AVStream *stream = self->recording_output.video_stream; + AVCodecContext *codec_context = self->video_codec_context; + + if(av_packet.stream_index == self->video_stream_index) { + av_packet.pts -= self->video_pts_offset; + av_packet.dts -= self->video_pts_offset; + } else { + gsr_recording_audio_stream *recording_start_audio = gsr_recording_output_get_audio_stream_by_index(&self->recording_output, av_packet.stream_index); + if(!recording_start_audio) { + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_replay_save: failed to find audio stream by index: %d", av_packet.stream_index); + free(replay_packet_data); + continue; + } + + const gsr_audio_track *audio_track = recording_start_audio->audio_track; + stream = recording_start_audio->stream; + codec_context = audio_track->codec_context; + + const gsr_audio_pts_offset *audio_pts_offset = &self->audio_pts_offsets[av_packet.stream_index - 1]; + assert(audio_pts_offset->stream_index == av_packet.stream_index); + av_packet.pts -= audio_pts_offset->pts_offset; + av_packet.dts -= audio_pts_offset->pts_offset; + } + + //av_packet.stream_index = stream->index; + av_packet_rescale_ts(&av_packet, codec_context->time_base, stream->time_base); + + const int ret = av_write_frame(self->recording_output.av_format_context, &av_packet); + if(ret < 0) + gsr_log(GSR_LOG_LEVEL_ERROR, "Failed to write frame index %d to muxer, reason: %s (%d)", av_packet.stream_index, gsr_av_error_to_string(ret), ret); + + free(replay_packet_data); + + //av_packet_free(&av_packet); + if(!gsr_replay_buffer_iterator_next(self->cloned_replay_buffer, &replay_iterator)) + break; + } + + gsr_recording_output_stop(&self->recording_output); + + self->success = success; + gsr_replay_save_cleanup(self); + self->finished = 1; + return NULL; +} + +bool gsr_replay_save_start(gsr_replay_save *self, AVCodecContext *video_codec_context, int video_stream_index, const gsr_audio_capture *audio_capture, gsr_encoder *encoder, const gsr_recorder_settings *settings, const char *file_extension, bool hdr, gsr_video_sources *video_sources, int current_save_replay_seconds) { + if(self->thread_created) + return true; + + self->encoder = encoder; + self->video_codec_context = video_codec_context; + self->video_stream_index = video_stream_index; + self->output_filepath[0] = '\0'; + self->success = false; + self->finished = 0; + + pthread_mutex_lock(&encoder->replay_mutex); + self->cloned_replay_buffer = gsr_replay_buffer_clone(encoder->replay_buffer); + pthread_mutex_unlock(&encoder->replay_mutex); + if(!self->cloned_replay_buffer) { + /* TODO: Return this error to mark the replay as failed */ + gsr_log(GSR_LOG_LEVEL_ERROR, "failed to save replay: failed to clone replay buffer"); + return false; + } + + const gsr_replay_buffer_iterator start_iterator = {0, 0}; + const gsr_replay_buffer_iterator search_start_iterator = current_save_replay_seconds == GSR_SAVE_REPLAY_SECONDS_FULL ? start_iterator : gsr_replay_buffer_find_packet_index_by_time_passed(self->cloned_replay_buffer, current_save_replay_seconds); + self->video_start_iterator = gsr_replay_buffer_find_keyframe(self->cloned_replay_buffer, search_start_iterator, video_stream_index, false); + if(self->video_start_iterator.packet_index == (size_t)-1) { + gsr_log(GSR_LOG_LEVEL_ERROR, "failed to save replay: failed to find a video keyframe. perhaps replay was saved too fast, before anything has been recorded"); + gsr_replay_save_cleanup(self); + return true; + } + + self->video_pts_offset = gsr_replay_buffer_iterator_get_packet(self->cloned_replay_buffer, self->video_start_iterator)->pts; + + if(audio_capture->num_tracks > 0) { + self->audio_pts_offsets = calloc(audio_capture->num_tracks, sizeof(gsr_audio_pts_offset)); + if(!self->audio_pts_offsets) { + gsr_log(GSR_LOG_LEVEL_ERROR, "failed to save replay: failed to allocate audio pts offsets"); + gsr_replay_save_cleanup(self); + return false; + } + } + + for(size_t i = 0; i < audio_capture->num_tracks; ++i) { + const gsr_audio_track *audio_track = &audio_capture->tracks[i]; + const gsr_replay_buffer_iterator audio_start_iterator = gsr_replay_buffer_find_keyframe(self->cloned_replay_buffer, self->video_start_iterator, audio_track->stream_index, false); + const int64_t audio_pts_offset = audio_start_iterator.packet_index == (size_t)-1 ? 0 : gsr_replay_buffer_iterator_get_packet(self->cloned_replay_buffer, audio_start_iterator)->pts; + self->audio_pts_offsets[i].pts_offset = audio_pts_offset; + self->audio_pts_offsets[i].stream_index = audio_track->stream_index; + ++self->num_audio_pts_offsets; + } + + if(!gsr_create_new_recording_filepath_from_timestamp(self->output_filepath, sizeof(self->output_filepath), settings->filename, "Replay", file_extension, settings->date_folders)) { + gsr_replay_save_cleanup(self); + return false; + } + + if(!gsr_recording_output_start(&self->recording_output, self->output_filepath, settings, video_codec_context, audio_capture, hdr, video_sources)) { + gsr_replay_save_cleanup(self); + return false; + } + + if(pthread_create(&self->thread, NULL, replay_save_thread, self) != 0) { + gsr_log(GSR_LOG_LEVEL_ERROR, "failed to save replay: failed to create thread"); + gsr_recording_output_stop(&self->recording_output); + gsr_replay_save_cleanup(self); + return false; + } + + self->thread_created = true; + return true; +} + +static bool gsr_replay_save_finish(gsr_replay_save *self, bool *success, const char **output_filepath) { + pthread_join(self->thread, NULL); + self->thread_created = false; + *success = self->success; + *output_filepath = self->output_filepath; + return true; +} + +bool gsr_replay_save_poll(gsr_replay_save *self, bool *success, const char **output_filepath) { + if(!self->thread_created || !self->finished) + return false; + + return gsr_replay_save_finish(self, success, output_filepath); +} + +bool gsr_replay_save_join(gsr_replay_save *self, bool *success, const char **output_filepath) { + if(!self->thread_created) + return false; + + return gsr_replay_save_finish(self, success, output_filepath); +} -- cgit v1.2.3 From 9dda64c39d0fdd6409305faff33c217dcf161e60 Mon Sep 17 00:00:00 2001 From: dec05eba Date: Sat, 1 Aug 2026 23:08:48 +0200 Subject: Move screenshot capture and CLI commands from main.cpp into separate modules The list commands return exit codes through args_parser instead of exiting. --- include/args_parser.h | 24 +- include/cli/commands.h | 15 ++ include/recorder/screenshot.h | 36 +++ meson.build | 2 + src/args_parser.c | 68 ++--- src/cli/commands.c | 372 +++++++++++++++++++++++++++ src/main.cpp | 581 +++--------------------------------------- src/recorder/screenshot.c | 221 ++++++++++++++++ 8 files changed, 734 insertions(+), 585 deletions(-) create mode 100644 include/cli/commands.h create mode 100644 include/recorder/screenshot.h create mode 100644 src/cli/commands.c create mode 100644 src/recorder/screenshot.c diff --git a/include/args_parser.h b/include/args_parser.h index 9e388ec..813d4c6 100644 --- a/include/args_parser.h +++ b/include/args_parser.h @@ -57,23 +57,31 @@ typedef struct { } typed_value; } Arg; +/* These return the exit code that gpu-screen-recorder should exit with */ typedef struct { - void (*version)(void *userdata); - void (*info)(void *userdata); - void (*list_audio_devices)(void *userdata); - void (*list_application_audio)(void *userdata); - void (*list_v4l2_devices)(void *userdata); - void (*list_capture_options)(const char *card_path, void *userdata); - void (*list_monitors)(void *userdata); + int (*version)(void *userdata); + int (*info)(void *userdata); + int (*list_audio_devices)(void *userdata); + int (*list_application_audio)(void *userdata); + int (*list_v4l2_devices)(void *userdata); + int (*list_capture_options)(const char *card_path, void *userdata); + int (*list_monitors)(void *userdata); } args_handlers; +typedef enum { + ARGS_PARSE_RESULT_ERROR, + ARGS_PARSE_RESULT_OK, + /* One of the |args_handlers| ran and |command_exit_code| is set */ + ARGS_PARSE_RESULT_COMMAND_HANDLED +} args_parse_result; + typedef struct { Arg args[NUM_ARGS]; gsr_recorder_settings settings; } args_parser; /* |argv| is stored as a reference */ -bool args_parser_parse(args_parser *self, int argc, char **argv, const args_handlers *args_handlers, void *userdata); +args_parse_result args_parser_parse(args_parser *self, int argc, char **argv, const args_handlers *args_handlers, void *userdata, int *command_exit_code); void args_parser_deinit(args_parser *self); bool args_parser_validate_with_gl_info(args_parser *self, gsr_egl *egl); diff --git a/include/cli/commands.h b/include/cli/commands.h new file mode 100644 index 0000000..9ab00f0 --- /dev/null +++ b/include/cli/commands.h @@ -0,0 +1,15 @@ +#ifndef GSR_CLI_COMMANDS_H +#define GSR_CLI_COMMANDS_H + +/* These are the |args_handlers| of the argument parser. They return the exit code that gpu-screen-recorder should exit with */ +int version_command(void *userdata); +int info_command(void *userdata); +int list_audio_devices_command(void *userdata); +int list_application_audio_command(void *userdata); +int list_v4l2_devices(void *userdata); +int list_capture_options_command(const char *card_path, void *userdata); +int list_monitors_command(void *userdata); + +void run_recording_saved_script_async(const char *script_file, const char *video_file, const char *type); + +#endif /* GSR_CLI_COMMANDS_H */ diff --git a/include/recorder/screenshot.h b/include/recorder/screenshot.h new file mode 100644 index 0000000..057383c --- /dev/null +++ b/include/recorder/screenshot.h @@ -0,0 +1,36 @@ +#ifndef GSR_RECORDER_SCREENSHOT_H +#define GSR_RECORDER_SCREENSHOT_H + +#include +#include +#include "../egl.h" +#include "../image_writer.h" +#include "../plugins.h" +#include "capture_source.h" +#include "capture_setup.h" +#include "settings.h" + +typedef struct { + const gsr_recorder_settings *settings; + gsr_egl *egl; + gsr_window *window; + gsr_capture_deps *capture_deps; + gsr_capture_sources *capture_sources; + gsr_image_format image_format; + const char **plugin_filepaths; + int num_plugin_filepaths; + const volatile sig_atomic_t *running; + void (*screenshot_saved)(const char *filepath, void *userdata); + void *userdata; +} gsr_screenshot_params; + +bool get_image_format_from_filename(const char *filename, gsr_image_format *image_format); +gsr_color_range image_format_to_color_range(gsr_image_format image_format, int image_quality); +int video_quality_to_image_quality_value(gsr_video_quality video_quality); + +/* Returns a |gsr_error| value. Captures one frame and writes it to |settings->filename| */ +int gsr_screenshot_take(const gsr_screenshot_params *params); + +int gsr_load_plugins(gsr_plugins *plugins, const char **plugin_filepaths, int num_plugin_filepaths, const gsr_recorder_settings *settings, gsr_egl *egl, vec2i video_size); + +#endif /* GSR_RECORDER_SCREENSHOT_H */ diff --git a/meson.build b/meson.build index 3b455a4..5bece63 100644 --- a/meson.build +++ b/meson.build @@ -43,6 +43,8 @@ src = [ 'src/recorder/recording_clock.c', 'src/recorder/muxer.c', 'src/recorder/replay_save.c', + 'src/recorder/screenshot.c', + 'src/cli/commands.c', 'src/egl.c', 'src/cuda.c', 'src/window_texture.c', diff --git a/src/args_parser.c b/src/args_parser.c index b105f84..0adb5ef 100644 --- a/src/args_parser.c +++ b/src/args_parser.c @@ -474,62 +474,62 @@ static bool args_parser_set_values(args_parser *self) { return true; } -bool args_parser_parse(args_parser *self, int argc, char **argv, const args_handlers *arg_handlers, void *userdata) { +args_parse_result args_parser_parse(args_parser *self, int argc, char **argv, const args_handlers *arg_handlers, void *userdata, int *command_exit_code) { assert(arg_handlers); memset(self, 0, sizeof(*self)); if(argc <= 1) { usage_full(); - return false; + return ARGS_PARSE_RESULT_ERROR; } if(argc == 2 && (strcmp(argv[1], "-h") == 0 || strcmp(argv[1], "--help") == 0)) { usage_full(); - return false; + return ARGS_PARSE_RESULT_ERROR; } if(argc == 2 && strcmp(argv[1], "--info") == 0) { - arg_handlers->info(userdata); - return true; + *command_exit_code = arg_handlers->info(userdata); + return ARGS_PARSE_RESULT_COMMAND_HANDLED; } if(argc == 2 && strcmp(argv[1], "--list-audio-devices") == 0) { - arg_handlers->list_audio_devices(userdata); - return true; + *command_exit_code = arg_handlers->list_audio_devices(userdata); + return ARGS_PARSE_RESULT_COMMAND_HANDLED; } if(argc == 2 && strcmp(argv[1], "--list-application-audio") == 0) { - arg_handlers->list_application_audio(userdata); - return true; + *command_exit_code = arg_handlers->list_application_audio(userdata); + return ARGS_PARSE_RESULT_COMMAND_HANDLED; } if(argc == 2 && strcmp(argv[1], "--list-v4l2-devices") == 0) { - arg_handlers->list_v4l2_devices(userdata); - return true; + *command_exit_code = arg_handlers->list_v4l2_devices(userdata); + return ARGS_PARSE_RESULT_COMMAND_HANDLED; } if(strcmp(argv[1], "--list-capture-options") == 0) { if(argc == 2) { - arg_handlers->list_capture_options(NULL, userdata); - return true; + *command_exit_code = arg_handlers->list_capture_options(NULL, userdata); + return ARGS_PARSE_RESULT_COMMAND_HANDLED; } else if(argc == 3 || argc == 4) { const char *card_path = argv[2]; - arg_handlers->list_capture_options(card_path, userdata); - return true; + *command_exit_code = arg_handlers->list_capture_options(card_path, userdata); + return ARGS_PARSE_RESULT_COMMAND_HANDLED; } else { gsr_log(GSR_LOG_LEVEL_ERROR, "expected --list-capture-options to be called with either no extra arguments or 1 extra argument (card path)"); - return false; + return ARGS_PARSE_RESULT_ERROR; } } if(strcmp(argv[1], "--list-monitors") == 0) { - arg_handlers->list_monitors(userdata); - return true; + *command_exit_code = arg_handlers->list_monitors(userdata); + return ARGS_PARSE_RESULT_COMMAND_HANDLED; } if(argc == 2 && strcmp(argv[1], "--version") == 0) { - arg_handlers->version(userdata); - return true; + *command_exit_code = arg_handlers->version(userdata); + return ARGS_PARSE_RESULT_COMMAND_HANDLED; } int arg_index = 0; @@ -579,19 +579,19 @@ bool args_parser_parse(args_parser *self, int argc, char **argv, const args_hand if(!arg) { gsr_log(GSR_LOG_LEVEL_ERROR, "invalid argument '%s'", arg_name); usage(); - return false; + return ARGS_PARSE_RESULT_ERROR; } if(arg->num_values > 0 && !arg->list) { gsr_log(GSR_LOG_LEVEL_ERROR, "expected argument '%s' to only be specified once", arg_name); usage(); - return false; + return ARGS_PARSE_RESULT_ERROR; } if(i + 1 >= argc) { gsr_log(GSR_LOG_LEVEL_ERROR, "missing value for argument '%s'", arg_name); usage(); - return false; + return ARGS_PARSE_RESULT_ERROR; } const char *arg_value = argv[i + 1]; @@ -607,7 +607,7 @@ bool args_parser_parse(args_parser *self, int argc, char **argv, const args_hand } else { gsr_log(GSR_LOG_LEVEL_ERROR, "%s should either be 'yes' or 'no', got: '%s'", arg_name, arg_value); usage(); - return false; + return ARGS_PARSE_RESULT_ERROR; } break; } @@ -617,7 +617,7 @@ bool args_parser_parse(args_parser *self, int argc, char **argv, const args_hand arg_get_expected_enum_names(arg, expected_enum_names, sizeof(expected_enum_names)); gsr_log(GSR_LOG_LEVEL_ERROR, "%s should either be %s, got: '%s'", arg_name, expected_enum_names, arg_value); usage(); - return false; + return ARGS_PARSE_RESULT_ERROR; } break; } @@ -625,19 +625,19 @@ bool args_parser_parse(args_parser *self, int argc, char **argv, const args_hand if(sscanf(arg_value, "%" PRIi64, &arg->typed_value.i64_value) != 1) { gsr_log(GSR_LOG_LEVEL_ERROR, "%s argument \"%s\" is not an integer", arg_name, arg_value); usage(); - return false; + return ARGS_PARSE_RESULT_ERROR; } if(arg->typed_value.i64_value < arg->integer_value_min) { gsr_log(GSR_LOG_LEVEL_ERROR, "%s argument is expected to be larger than %" PRIi64 ", got %" PRIi64, arg_name, arg->integer_value_min, arg->typed_value.i64_value); usage(); - return false; + return ARGS_PARSE_RESULT_ERROR; } if(arg->typed_value.i64_value > arg->integer_value_max) { gsr_log(GSR_LOG_LEVEL_ERROR, "%s argument is expected to be less than %" PRIi64 ", got %" PRIi64, arg_name, arg->integer_value_max, arg->typed_value.i64_value); usage(); - return false; + return ARGS_PARSE_RESULT_ERROR; } break; } @@ -645,19 +645,19 @@ bool args_parser_parse(args_parser *self, int argc, char **argv, const args_hand if(sscanf(arg_value, "%lf", &arg->typed_value.d_value) != 1) { gsr_log(GSR_LOG_LEVEL_ERROR, "%s argument \"%s\" is not an floating-point number", arg_name, arg_value); usage(); - return false; + return ARGS_PARSE_RESULT_ERROR; } if(arg->typed_value.d_value < arg->integer_value_min) { gsr_log(GSR_LOG_LEVEL_ERROR, "%s argument is expected to be larger than %" PRIi64 ", got %lf", arg_name, arg->integer_value_min, arg->typed_value.d_value); usage(); - return false; + return ARGS_PARSE_RESULT_ERROR; } if(arg->typed_value.d_value > arg->integer_value_max) { gsr_log(GSR_LOG_LEVEL_ERROR, "%s argument is expected to be less than %" PRIi64 ", got %lf", arg_name, arg->integer_value_max, arg->typed_value.d_value); usage(); - return false; + return ARGS_PARSE_RESULT_ERROR; } break; } @@ -665,7 +665,7 @@ bool args_parser_parse(args_parser *self, int argc, char **argv, const args_hand if(!arg_append_value(arg, arg_value)) { gsr_log(GSR_LOG_LEVEL_ERROR, "failed to append argument, out of memory"); - return false; + return ARGS_PARSE_RESULT_ERROR; } } @@ -674,11 +674,11 @@ bool args_parser_parse(args_parser *self, int argc, char **argv, const args_hand if(!arg->optional && arg->num_values == 0) { gsr_log(GSR_LOG_LEVEL_ERROR, "missing argument '%s'", arg->key); usage(); - return false; + return ARGS_PARSE_RESULT_ERROR; } } - return args_parser_set_values(self); + return args_parser_set_values(self) ? ARGS_PARSE_RESULT_OK : ARGS_PARSE_RESULT_ERROR; } void args_parser_deinit(args_parser *self) { diff --git a/src/cli/commands.c b/src/cli/commands.c new file mode 100644 index 0000000..4eaaf65 --- /dev/null +++ b/src/cli/commands.c @@ -0,0 +1,372 @@ +#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 +#include +#include +#include +#include + +#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; + } + 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 = {false, false, false}; + if(gsr_windowing_init(&windowing, &windowing_params) != GSR_ERROR_OK) + return 1; + if(gsr_windowing_load_egl(&windowing, &windowing_params) != GSR_ERROR_OK) + 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); + + // Not needed as this will just slow down shutdown + //gsr_egl_unload(&egl); + //gsr_window_destroy(&window); + //if(dpy) + // XCloseDisplay(dpy); + + 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 = {false, false, false}; + 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) + 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); + + // Not needed as this will just slow down shutdown + //gsr_egl_unload(&egl); + //gsr_window_destroy(&window); + //if(dpy) + // XCloseDisplay(dpy); + + return 0; +} + +int list_monitors_command(void *userdata) { + (void)userdata; + gsr_windowing windowing; + const gsr_windowing_params windowing_params = {false, false, false}; + if(gsr_windowing_init(&windowing, &windowing_params) != GSR_ERROR_OK) + return 1; + if(gsr_windowing_load_egl(&windowing, &windowing_params) != GSR_ERROR_OK) + return 22; + + if(windowing.card_path_found) + list_monitors(windowing.window, windowing.egl.card_path); + + fflush(stdout); + + // Not needed as this will just slow down shutdown + //gsr_egl_unload(&egl); + //gsr_window_destroy(&window); + //if(dpy) + // XCloseDisplay(dpy); + + return 0; +} diff --git a/src/main.cpp b/src/main.cpp index 6a030aa..46b54e2 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -10,6 +10,8 @@ extern "C" { #include "../include/recorder/audio_capture.h" #include "../include/recorder/muxer.h" #include "../include/recorder/replay_save.h" +#include "../include/recorder/screenshot.h" +#include "../include/cli/commands.h" #include "../include/recorder/error.h" #include "../include/capture/nvfbc.h" #include "../include/capture/xcomposite.h" @@ -136,539 +138,12 @@ static void save_replay_30_minutes_handler(int) { save_replay_seconds = 60*30; } -static 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); - } -} - // TODO: Cleanup -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; - } - 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"); -} - -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) - std::swap(monitor_size.x, monitor_size.y); - 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)); -} - // Returns the number of monitors found -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 -} - -static void version_command(void *userdata) { - (void)userdata; - puts(GSR_VERSION); - fflush(stdout); - _exit(0); -} - -static void info_command(void *userdata) { - (void)userdata; - gsr_windowing windowing; - const gsr_windowing_params windowing_params = {false, false, false}; - if(gsr_windowing_init(&windowing, &windowing_params) != GSR_ERROR_OK) - _exit(1); - if(gsr_windowing_load_egl(&windowing, &windowing_params) != GSR_ERROR_OK) - _exit(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); - - // Not needed as this will just slow down shutdown - //gsr_egl_unload(&egl); - //gsr_window_destroy(&window); - //if(dpy) - // XCloseDisplay(dpy); - - _exit(0); -} - -static void 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); - _exit(0); -} - -static bool app_audio_query_callback(const char *app_name, void*) { - puts(app_name); - return true; -} - -static void 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); - _exit(0); -} - -static void list_v4l2_devices(void *userdata) { - (void)userdata; - gsr_capture_v4l2_list_devices(camera_query_callback, NULL); - - fflush(stdout); - _exit(0); -} - // |card_path| can be NULL. If not NULL then |vendor| has to be valid -static void list_capture_options_command(const char *card_path, void *userdata) { - (void)userdata; - gsr_windowing windowing; - const gsr_windowing_params windowing_params = {false, false, false}; - if(gsr_windowing_init(&windowing, &windowing_params) != GSR_ERROR_OK) - _exit(1); - if(!card_path && gsr_windowing_load_egl(&windowing, &windowing_params) != GSR_ERROR_OK) - _exit(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); - - // Not needed as this will just slow down shutdown - //gsr_egl_unload(&egl); - //gsr_window_destroy(&window); - //if(dpy) - // XCloseDisplay(dpy); - - _exit(0); -} - -static void list_monitors_command(void *userdata) { - (void)userdata; - gsr_windowing windowing; - const gsr_windowing_params windowing_params = {false, false, false}; - if(gsr_windowing_init(&windowing, &windowing_params) != GSR_ERROR_OK) - _exit(1); - if(gsr_windowing_load_egl(&windowing, &windowing_params) != GSR_ERROR_OK) - _exit(22); - - if(windowing.card_path_found) - list_monitors(windowing.window, windowing.egl.card_path); - - fflush(stdout); - - // Not needed as this will just slow down shutdown - //gsr_egl_unload(&egl); - //gsr_window_destroy(&window); - //if(dpy) - // XCloseDisplay(dpy); - - _exit(0); -} - static gsr_capture_deps capture_deps; -static gsr_color_range image_format_to_color_range(gsr_image_format image_format, int image_quality) { - switch(image_format) { - case GSR_IMAGE_FORMAT_JPEG: return image_quality >= JPEG_YUV444_QUALITY_THRESHOLD ? GSR_COLOR_RANGE_FULL : GSR_COLOR_RANGE_LIMITED; - case GSR_IMAGE_FORMAT_PNG: return GSR_COLOR_RANGE_FULL; - } - assert(false); - return GSR_COLOR_RANGE_FULL; -} - -static int video_quality_to_image_quality_value(gsr_video_quality video_quality) { - switch(video_quality) { - case GSR_VIDEO_QUALITY_MEDIUM: - return 75; - case GSR_VIDEO_QUALITY_HIGH: - return 85; - case GSR_VIDEO_QUALITY_VERY_HIGH: - return JPEG_YUV444_QUALITY_THRESHOLD; // Quality above 90 makes the jpeg image encoder (stb_image_writer) use yuv444 instead of yuv420, which greatly improves small colored text quality on dark background - case GSR_VIDEO_QUALITY_ULTRA: - return 97; - } - assert(false); - return 90; -} - -static void load_plugins(gsr_plugins *plugins, args_parser &arg_parser, gsr_egl *egl, vec2i video_size) { - const Arg *plugin_arg = args_parser_get_arg(&arg_parser, "-p"); - assert(plugin_arg); - - if(plugin_arg->num_values > 0) { - const gsr_color_depth color_depth = video_codec_to_bit_depth(arg_parser.settings.video_codec); - assert(color_depth == GSR_COLOR_DEPTH_8_BITS || color_depth == GSR_COLOR_DEPTH_10_BITS); - - const gsr_plugin_init_params plugin_init_params = { - (unsigned int)video_size.x, - (unsigned int)video_size.y, - (unsigned int)arg_parser.settings.fps, - color_depth == GSR_COLOR_DEPTH_8_BITS ? GSR_PLUGIN_COLOR_DEPTH_8_BITS : GSR_PLUGIN_COLOR_DEPTH_10_BITS, - egl->context_type == GSR_GL_CONTEXT_TYPE_GLX ? GSR_PLUGIN_GRAPHICS_API_GLX : GSR_PLUGIN_GRAPHICS_API_EGL_ES, - }; - - if(!gsr_plugins_init(plugins, plugin_init_params, egl)) - _exit(1); - - for(int i = 0; i < plugin_arg->num_values; ++i) { - if(!gsr_plugins_load_plugin(plugins, plugin_arg->values[i])) - _exit(1); - } - } -} - // TODO: 10-bit and hdr. -static void capture_image_to_file(args_parser &arg_parser, gsr_egl *egl, gsr_window *window, gsr_image_format image_format, gsr_capture_sources *capture_sources) { - const int image_quality = video_quality_to_image_quality_value(arg_parser.settings.video_quality); - const gsr_color_range color_range = image_format_to_color_range(image_format, image_quality); - arg_parser.settings.fps = 60; // We want to capture an image as soon as possible - - vec2i video_size = {0, 0}; - gsr_video_sources video_sources_data; - const int video_sources_result = gsr_video_sources_create(&video_sources_data, &arg_parser.settings, egl, &capture_deps, true, capture_sources, &video_size); - if(video_sources_result != GSR_ERROR_OK) - _exit(gsr_error_to_exit_code(video_sources_result)); - gsr_video_sources *video_sources = &video_sources_data; - gsr_video_sources_update_with_real_video_size(video_sources, video_size); - - const Arg *plugin_arg = args_parser_get_arg(&arg_parser, "-p"); - assert(plugin_arg); - - gsr_plugins plugins; - memset(&plugins, 0, sizeof(plugins)); - - if(plugin_arg->num_values > 0) - load_plugins(&plugins, arg_parser, egl, video_size); - - gsr_image_writer image_writer; - if(!gsr_image_writer_init_opengl(&image_writer, egl, video_size.x, video_size.y)) { - gsr_log(GSR_LOG_LEVEL_ERROR, "capture_image_to_file: gsr_image_write_gl_init failed"); - _exit(1); - } - - gsr_color_conversion_params color_conversion_params; - memset(&color_conversion_params, 0, sizeof(color_conversion_params)); - color_conversion_params.color_range = color_range; - color_conversion_params.egl = egl; - color_conversion_params.load_external_image_shader = gsr_video_sources_uses_external_image(video_sources); - - color_conversion_params.destination_textures[0] = image_writer.texture; - color_conversion_params.destination_textures_size[0] = video_size; - color_conversion_params.num_destination_textures = 1; - color_conversion_params.destination_color = GSR_DESTINATION_COLOR_RGB; - - gsr_color_conversion color_conversion; - if(gsr_color_conversion_init(&color_conversion, &color_conversion_params) != 0) { - gsr_log(GSR_LOG_LEVEL_ERROR, "capture_image_to_file: failed to create color conversion"); - _exit(1); - } - - gsr_color_conversion_clear(&color_conversion); - - gsr_color_conversion *output_color_conversion = plugins.num_plugins > 0 ? &plugins.color_conversion : &color_conversion; - - bool should_stop_error = false; - egl->glClear(0); - - while(running) { - while(gsr_window_process_event(window)) { - if(capture_deps.x11_cursor_display && arg_parser.settings.record_cursor) - gsr_cursor_on_event(&capture_deps.x11_cursor, gsr_window_get_event_data(window)); - - for(size_t video_source_index = 0; video_source_index < video_sources->num_items; ++video_source_index) { - gsr_video_source &video_source = video_sources->items[video_source_index]; - gsr_capture_on_event(video_source.capture, egl); - } - } - - if(capture_deps.x11_cursor_display && arg_parser.settings.record_cursor) - gsr_cursor_tick(&capture_deps.x11_cursor, DefaultRootWindow(capture_deps.x11_cursor_display)); - - gsr_capture_deps_cleanup_kms_fds(&capture_deps); - - gsr_capture_deps_update_kms(&capture_deps); - - should_stop_error = false; - for(size_t video_source_index = 0; video_source_index < video_sources->num_items; ++video_source_index) { - gsr_video_source &video_source = video_sources->items[video_source_index]; - gsr_capture_tick(video_source.capture); - if(gsr_capture_should_stop(video_source.capture, &should_stop_error)) { - running = 0; - break; - } - } - - for(size_t video_source_index = 0; video_source_index < video_sources->num_items; ++video_source_index) { - gsr_video_source &video_source = video_sources->items[video_source_index]; - if(video_source.capture->pre_capture) - video_source.capture->pre_capture(video_source.capture, &video_source.metadata, output_color_conversion); - } - - if(output_color_conversion->schedule_clear) { - output_color_conversion->schedule_clear = false; - gsr_color_conversion_clear(output_color_conversion); - } - - bool all_sources_captured = true; - for(size_t video_source_index = 0; video_source_index < video_sources->num_items; ++video_source_index) { - gsr_video_source &video_source = video_sources->items[video_source_index]; - // It can fail, for example when capturing portal and the target is a monitor that hasn't been updated. - // This can also happen for example if the system suspends and the monitor to capture's framebuffer is gone, or if the target window disappeared. - if(gsr_capture_capture(video_source.capture, &video_source.metadata, output_color_conversion) != 0) - all_sources_captured = false; - } - - gsr_capture_deps_cleanup_kms_fds(&capture_deps); - - if(all_sources_captured) - break; - - if(running) - usleep(30 * 1000); // 30 ms - } - - if(plugins.num_plugins > 0) { - gsr_plugins_draw(&plugins); - gsr_color_conversion_draw(&color_conversion, plugins.texture, - {0, 0}, video_size, - {0, 0}, video_size, - video_size, GSR_ROT_0, GSR_FLIP_NONE, GSR_SOURCE_COLOR_RGB, false); - } - - gsr_egl_swap_buffers(egl); - - if(!should_stop_error) { - if(!gsr_image_writer_write_to_file(&image_writer, arg_parser.settings.filename, image_format, image_quality)) { - gsr_log(GSR_LOG_LEVEL_ERROR, "capture_image_to_file: failed to write opengl texture to image output file %s", arg_parser.settings.filename); - _exit(1); - } - - if(arg_parser.settings.recording_saved_script) - run_recording_saved_script_async(arg_parser.settings.recording_saved_script, arg_parser.settings.filename, "screenshot"); - } - - gsr_plugins_deinit(&plugins); - gsr_image_writer_deinit(&image_writer); - gsr_video_sources_deinit(video_sources); - _exit(should_stop_error ? 3 : 0); -} - // Manually check if the audio inputs we give exist. This is only needed for pipewire, not pulseaudio. // Pipewire instead DEFAULTS TO THE DEFAULT AUDIO INPUT. THAT'S RETARDED. // OH, YOU MISSPELLED THE AUDIO INPUT? FUCK YOU @@ -677,16 +152,10 @@ static void capture_image_to_file(args_parser &arg_parser, gsr_egl *egl, gsr_win #ifdef GSR_APP_AUDIO #endif -static bool get_image_format_from_filename(const char *filename, gsr_image_format *image_format) { - if(gsr_string_ends_with(filename, ".jpg") || gsr_string_ends_with(filename, ".jpeg")) { - *image_format = GSR_IMAGE_FORMAT_JPEG; - return true; - } else if(gsr_string_ends_with(filename, ".png")) { - *image_format = GSR_IMAGE_FORMAT_PNG; - return true; - } else { - return false; - } +static void screenshot_saved_callback(const char *filepath, void *userdata) { + const char *recording_saved_script = (const char*)userdata; + if(recording_saved_script) + run_recording_saved_script_async(recording_saved_script, filepath, "screenshot"); } static void set_display_server_environment_variables() { @@ -835,8 +304,15 @@ int main(int argc, char **argv) { arg_handlers.list_monitors = list_monitors_command; args_parser arg_parser; - if(!args_parser_parse(&arg_parser, argc, argv, &arg_handlers, NULL)) - _exit(1); + 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(1); + case ARGS_PARSE_RESULT_COMMAND_HANDLED: + _exit(command_exit_code); + case ARGS_PARSE_RESULT_OK: + break; + } if(!arg_parser.settings.low_power) { // Forces low latency encoding mode. Use this environment variable until vaapi supports setting this as a parameter. @@ -966,8 +442,27 @@ int main(int argc, char **argv) { _exit(1); } - capture_image_to_file(arg_parser, &egl, window, image_format, capture_sources); - _exit(0); + const Arg *screenshot_plugin_arg = args_parser_get_arg(&arg_parser, "-p"); + assert(screenshot_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 = &egl; + screenshot_params.window = window; + screenshot_params.capture_deps = &capture_deps; + screenshot_params.capture_sources = capture_sources; + screenshot_params.image_format = image_format; + screenshot_params.plugin_filepaths = screenshot_plugin_arg->values; + screenshot_params.num_plugin_filepaths = screenshot_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; + + const int screenshot_result = gsr_screenshot_take(&screenshot_params); + _exit(gsr_error_to_exit_code(screenshot_result)); } AVFormatContext *av_format_context; @@ -1088,8 +583,8 @@ int main(int argc, char **argv) { gsr_plugins plugins; memset(&plugins, 0, sizeof(plugins)); - if(plugin_arg->num_values > 0) - load_plugins(&plugins, arg_parser, &egl, video_size); + if(gsr_load_plugins(&plugins, plugin_arg->values, plugin_arg->num_values, &arg_parser.settings, &egl, video_size) != GSR_ERROR_OK) + _exit(1); gsr_color_conversion_params color_conversion_params; memset(&color_conversion_params, 0, sizeof(color_conversion_params)); diff --git a/src/recorder/screenshot.c b/src/recorder/screenshot.c new file mode 100644 index 0000000..45155b4 --- /dev/null +++ b/src/recorder/screenshot.c @@ -0,0 +1,221 @@ +#include "../../include/recorder/screenshot.h" +#include "../../include/recorder/error.h" +#include "../../include/color_conversion.h" +#include "../../include/window/window.h" +#include "../../include/log.h" + +#include +#include +#include + +#include + +#define JPEG_YUV444_QUALITY_THRESHOLD 91 + +gsr_color_range image_format_to_color_range(gsr_image_format image_format, int image_quality) { + switch(image_format) { + case GSR_IMAGE_FORMAT_JPEG: return image_quality >= JPEG_YUV444_QUALITY_THRESHOLD ? GSR_COLOR_RANGE_FULL : GSR_COLOR_RANGE_LIMITED; + case GSR_IMAGE_FORMAT_PNG: return GSR_COLOR_RANGE_FULL; + } + assert(false); + return GSR_COLOR_RANGE_FULL; +} + +int video_quality_to_image_quality_value(gsr_video_quality video_quality) { + switch(video_quality) { + case GSR_VIDEO_QUALITY_MEDIUM: + return 75; + case GSR_VIDEO_QUALITY_HIGH: + return 85; + case GSR_VIDEO_QUALITY_VERY_HIGH: + return JPEG_YUV444_QUALITY_THRESHOLD; // Quality above 90 makes the jpeg image encoder (stb_image_writer) use yuv444 instead of yuv420, which greatly improves small colored text quality on dark background + case GSR_VIDEO_QUALITY_ULTRA: + return 97; + } + assert(false); + return 90; +} + +int gsr_load_plugins(gsr_plugins *plugins, const char **plugin_filepaths, int num_plugin_filepaths, const gsr_recorder_settings *settings, gsr_egl *egl, vec2i video_size) { + if(num_plugin_filepaths == 0) + return GSR_ERROR_OK; + + const gsr_color_depth color_depth = video_codec_to_bit_depth(settings->video_codec); + assert(color_depth == GSR_COLOR_DEPTH_8_BITS || color_depth == GSR_COLOR_DEPTH_10_BITS); + + gsr_plugin_init_params plugin_init_params; + plugin_init_params.width = video_size.x; + plugin_init_params.height = video_size.y; + plugin_init_params.fps = settings->fps; + plugin_init_params.color_depth = color_depth == GSR_COLOR_DEPTH_8_BITS ? GSR_PLUGIN_COLOR_DEPTH_8_BITS : GSR_PLUGIN_COLOR_DEPTH_10_BITS; + plugin_init_params.graphics_api = egl->context_type == GSR_GL_CONTEXT_TYPE_GLX ? GSR_PLUGIN_GRAPHICS_API_GLX : GSR_PLUGIN_GRAPHICS_API_EGL_ES; + + if(!gsr_plugins_init(plugins, plugin_init_params, egl)) + return GSR_ERROR_GENERIC; + + for(int i = 0; i < num_plugin_filepaths; ++i) { + if(!gsr_plugins_load_plugin(plugins, plugin_filepaths[i])) + return GSR_ERROR_GENERIC; + } + + return GSR_ERROR_OK; +} + +int gsr_screenshot_take(const gsr_screenshot_params *params) { + const gsr_recorder_settings *settings = params->settings; + gsr_egl *egl = params->egl; + gsr_window *window = params->window; + gsr_capture_deps *capture_deps = params->capture_deps; + gsr_capture_sources *capture_sources = params->capture_sources; + const volatile sig_atomic_t *running = params->running; + const int image_quality = video_quality_to_image_quality_value(settings->video_quality); + const gsr_color_range color_range = image_format_to_color_range(params->image_format, image_quality); + + vec2i video_size = {0, 0}; + gsr_video_sources video_sources_data; + const int video_sources_result = gsr_video_sources_create(&video_sources_data, settings, egl, capture_deps, true, capture_sources, &video_size); + if(video_sources_result != GSR_ERROR_OK) + return video_sources_result; + gsr_video_sources *video_sources = &video_sources_data; + gsr_video_sources_update_with_real_video_size(video_sources, video_size); + + gsr_plugins plugins; + memset(&plugins, 0, sizeof(plugins)); + + const int load_plugins_result = gsr_load_plugins(&plugins, params->plugin_filepaths, params->num_plugin_filepaths, settings, egl, video_size); + if(load_plugins_result != GSR_ERROR_OK) { + gsr_video_sources_deinit(video_sources); + return load_plugins_result; + } + + gsr_image_writer image_writer; + if(!gsr_image_writer_init_opengl(&image_writer, egl, video_size.x, video_size.y)) { + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_screenshot_take: gsr_image_write_gl_init failed"); + gsr_plugins_deinit(&plugins); + gsr_video_sources_deinit(video_sources); + return GSR_ERROR_GENERIC; + } + + gsr_color_conversion_params color_conversion_params; + memset(&color_conversion_params, 0, sizeof(color_conversion_params)); + color_conversion_params.color_range = color_range; + color_conversion_params.egl = egl; + color_conversion_params.load_external_image_shader = gsr_video_sources_uses_external_image(video_sources); + + color_conversion_params.destination_textures[0] = image_writer.texture; + color_conversion_params.destination_textures_size[0] = video_size; + color_conversion_params.num_destination_textures = 1; + color_conversion_params.destination_color = GSR_DESTINATION_COLOR_RGB; + + gsr_color_conversion color_conversion; + if(gsr_color_conversion_init(&color_conversion, &color_conversion_params) != 0) { + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_screenshot_take: failed to create color conversion"); + gsr_image_writer_deinit(&image_writer); + gsr_plugins_deinit(&plugins); + gsr_video_sources_deinit(video_sources); + return GSR_ERROR_GENERIC; + } + + gsr_color_conversion_clear(&color_conversion); + + gsr_color_conversion *output_color_conversion = plugins.num_plugins > 0 ? &plugins.color_conversion : &color_conversion; + + bool should_stop_error = false; + egl->glClear(0); + + while(*running) { + while(gsr_window_process_event(window)) { + if(capture_deps->x11_cursor_display && settings->record_cursor) + gsr_cursor_on_event(&capture_deps->x11_cursor, gsr_window_get_event_data(window)); + + for(size_t video_source_index = 0; video_source_index < video_sources->num_items; ++video_source_index) { + gsr_video_source *video_source = &video_sources->items[video_source_index]; + gsr_capture_on_event(video_source->capture, egl); + } + } + + if(capture_deps->x11_cursor_display && settings->record_cursor) + gsr_cursor_tick(&capture_deps->x11_cursor, DefaultRootWindow(capture_deps->x11_cursor_display)); + + gsr_capture_deps_cleanup_kms_fds(capture_deps); + + gsr_capture_deps_update_kms(capture_deps); + + should_stop_error = false; + for(size_t video_source_index = 0; video_source_index < video_sources->num_items; ++video_source_index) { + gsr_video_source *video_source = &video_sources->items[video_source_index]; + gsr_capture_tick(video_source->capture); + if(gsr_capture_should_stop(video_source->capture, &should_stop_error)) { + break; + break; + } + } + + for(size_t video_source_index = 0; video_source_index < video_sources->num_items; ++video_source_index) { + gsr_video_source *video_source = &video_sources->items[video_source_index]; + if(video_source->capture->pre_capture) + video_source->capture->pre_capture(video_source->capture, &video_source->metadata, output_color_conversion); + } + + if(output_color_conversion->schedule_clear) { + output_color_conversion->schedule_clear = false; + gsr_color_conversion_clear(output_color_conversion); + } + + bool all_sources_captured = true; + for(size_t video_source_index = 0; video_source_index < video_sources->num_items; ++video_source_index) { + gsr_video_source *video_source = &video_sources->items[video_source_index]; + // It can fail, for example when capturing portal and the target is a monitor that hasn't been updated. + // This can also happen for example if the system suspends and the monitor to capture's framebuffer is gone, or if the target window disappeared. + if(gsr_capture_capture(video_source->capture, &video_source->metadata, output_color_conversion) != 0) + all_sources_captured = false; + } + + gsr_capture_deps_cleanup_kms_fds(capture_deps); + + if(all_sources_captured) + break; + + if(*running) + usleep(30 * 1000); // 30 ms + } + + if(plugins.num_plugins > 0) { + gsr_plugins_draw(&plugins); + gsr_color_conversion_draw(&color_conversion, plugins.texture, + (vec2i){0, 0}, video_size, + (vec2i){0, 0}, video_size, + video_size, GSR_ROT_0, GSR_FLIP_NONE, GSR_SOURCE_COLOR_RGB, false); + } + + gsr_egl_swap_buffers(egl); + + int result = should_stop_error ? GSR_ERROR_CAPTURE_FAILED : GSR_ERROR_OK; + if(!should_stop_error) { + if(!gsr_image_writer_write_to_file(&image_writer, settings->filename, params->image_format, image_quality)) { + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_screenshot_take: failed to write opengl texture to image output file %s", settings->filename); + result = GSR_ERROR_GENERIC; + } + + if(result == GSR_ERROR_OK && params->screenshot_saved) + params->screenshot_saved(settings->filename, params->userdata); + } + + gsr_color_conversion_deinit(&color_conversion); + gsr_plugins_deinit(&plugins); + gsr_image_writer_deinit(&image_writer); + gsr_video_sources_deinit(video_sources); + return result; +} + +bool get_image_format_from_filename(const char *filename, gsr_image_format *image_format) { + if(gsr_string_ends_with(filename, ".jpg") || gsr_string_ends_with(filename, ".jpeg")) { + *image_format = GSR_IMAGE_FORMAT_JPEG; + return true; + } else if(gsr_string_ends_with(filename, ".png")) { + *image_format = GSR_IMAGE_FORMAT_PNG; + return true; + } else { + return false; + } +} -- cgit v1.2.3 From 8272e61823e04733123a17a47645e7097eac45c0 Mon Sep 17 00:00:00 2001 From: dec05eba Date: Sat, 1 Aug 2026 23:22:38 +0200 Subject: Rewrite main.cpp in C as a thin CLI on top of a new gsr_recorder library API The recording setup, record loop and teardown moved into a gsr_recorder pseudo class that reports saved recordings through callbacks, so the CLI only parses arguments, handles signals and prints results. The project is now built as pure C. --- include/recorder/recorder.h | 54 ++ meson.build | 19 +- src/cli/main.c | 562 +++++++++++++++++++++ src/main.cpp | 1158 ------------------------------------------- src/recorder/recorder.c | 826 ++++++++++++++++++++++++++++++ 5 files changed, 1452 insertions(+), 1167 deletions(-) create mode 100644 include/recorder/recorder.h create mode 100644 src/cli/main.c delete mode 100644 src/main.cpp create mode 100644 src/recorder/recorder.c diff --git a/include/recorder/recorder.h b/include/recorder/recorder.h new file mode 100644 index 0000000..bbd37e1 --- /dev/null +++ b/include/recorder/recorder.h @@ -0,0 +1,54 @@ +#ifndef GSR_RECORDER_RECORDER_H +#define GSR_RECORDER_RECORDER_H + +#include +#include "settings.h" +#include "capture_source.h" +#include "capture_setup.h" +#include "audio_input.h" +#include "windowing.h" +#ifdef GSR_APP_AUDIO +#include "../pipewire_audio.h" +#endif + +/* Records video and audio to a file, or to a replay buffer that can be saved to a file at any time */ +typedef struct gsr_recorder gsr_recorder; + +typedef struct { + /* |filepath| is NULL when the recording failed to save */ + void (*replay_saved)(const char *filepath, void *userdata); + void (*recording_started)(const char *filepath, void *userdata); + /* |filepath| is NULL when the recording failed to save */ + void (*recording_stopped)(const char *filepath, void *userdata); + void (*paused_changed)(bool paused, void *userdata); + void *userdata; +} gsr_recorder_callbacks; + +typedef struct { + const gsr_recorder_settings *settings; + gsr_windowing *windowing; + gsr_capture_deps *capture_deps; + gsr_capture_sources *capture_sources; + gsr_audio_input_tracks *audio_input_tracks; + const char **plugin_filepaths; + int num_plugin_filepaths; +#ifdef GSR_APP_AUDIO + gsr_pipewire_audio *pipewire_audio; +#endif +} gsr_recorder_params; + +/* Returns NULL on failure and sets |error| to a |gsr_error| value */ +gsr_recorder* gsr_recorder_create(const gsr_recorder_params *params, const gsr_recorder_callbacks *callbacks, int *error); +void gsr_recorder_destroy(gsr_recorder *self); + +/* Returns a |gsr_error| value. Records until gsr_recorder_stop is called or until the capture target is gone */ +int gsr_recorder_run(gsr_recorder *self); + +/* These are safe to call from a signal handler or from another thread */ +void gsr_recorder_stop(gsr_recorder *self); +void gsr_recorder_toggle_pause(gsr_recorder *self); +void gsr_recorder_toggle_replay_recording(gsr_recorder *self); +/* |seconds| can be GSR_SAVE_REPLAY_SECONDS_FULL to save the whole replay buffer */ +void gsr_recorder_save_replay(gsr_recorder *self, int seconds); + +#endif /* GSR_RECORDER_RECORDER_H */ diff --git a/meson.build b/meson.build index 5bece63..0653470 100644 --- a/meson.build +++ b/meson.build @@ -1,10 +1,10 @@ -project('gpu-screen-recorder', ['c', 'cpp'], version : '5.15.3', default_options : ['warning_level=2', 'cpp_std=c++17']) +project('gpu-screen-recorder', 'c', version : '5.15.3', default_options : ['warning_level=2']) -add_project_arguments('-Wshadow', language : ['c', 'cpp']) +add_project_arguments('-Wshadow', language : 'c') if get_option('buildtype') == 'debug' - add_project_arguments('-g3', language : ['c', 'cpp']) + add_project_arguments('-g3', language : 'c') elif get_option('buildtype') == 'release' - add_project_arguments('-DNDEBUG', language : ['c', 'cpp']) + add_project_arguments('-DNDEBUG', language : 'c') endif src = [ @@ -44,6 +44,7 @@ src = [ 'src/recorder/muxer.c', 'src/recorder/replay_save.c', 'src/recorder/screenshot.c', + 'src/recorder/recorder.c', 'src/cli/commands.c', 'src/egl.c', 'src/cuda.c', @@ -61,7 +62,7 @@ src = [ 'src/plugins.c', 'src/wayland_host_bridge.c', 'src/sound.c', - 'src/main.cpp', + 'src/cli/main.c', ] subdir('protocol') @@ -105,7 +106,7 @@ if get_option('portal') == true 'src/dbus.c', 'src/pipewire_video.c', ] - add_project_arguments('-DGSR_PORTAL', language : ['c', 'cpp']) + add_project_arguments('-DGSR_PORTAL', language : 'c') uses_pipewire = true endif @@ -113,7 +114,7 @@ if get_option('app_audio') == true src += [ 'src/pipewire_audio.c', ] - add_project_arguments('-DGSR_APP_AUDIO', language : ['c', 'cpp']) + add_project_arguments('-DGSR_APP_AUDIO', language : 'c') uses_pipewire = true endif @@ -123,10 +124,10 @@ if uses_pipewire == true dependency('libspa-0.2'), dependency('dbus-1'), ] - add_project_arguments('-DGSR_DBUS', language : ['c', 'cpp']) + add_project_arguments('-DGSR_DBUS', language : 'c') endif -add_project_arguments('-DGSR_VERSION="' + meson.project_version() + '"', language: ['c', 'cpp']) +add_project_arguments('-DGSR_VERSION="' + meson.project_version() + '"', language: 'c') executable('gsr-kms-server', 'kms/server/kms_server.c', dependencies : dependency('libdrm'), c_args : '-fstack-protector-all', install : true) executable('gpu-screen-recorder', src, dependencies : dep, install : true) diff --git a/src/cli/main.c b/src/cli/main.c new file mode 100644 index 0000000..b74eb1c --- /dev/null +++ b/src/cli/main.c @@ -0,0 +1,562 @@ +/* + Copyright (C) 2020 dec05eba + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#include "../../include/cli/commands.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 +#include +#include +#include +#include +#include +#include +#include + +static volatile sig_atomic_t 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; + 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); + 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); + } +} + +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 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"); +} + +static void replay_saved_callback(const char *filepath, void *userdata) { + const char *recording_saved_script = userdata; + if(!filepath) { + printf("gsr error: Failed to save replay\n"); + fflush(stdout); + return; + } + + puts(filepath); + fflush(stdout); + if(recording_saved_script) + run_recording_saved_script_async(recording_saved_script, filepath, "replay"); +} + +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) { + const char *recording_saved_script = userdata; + if(!filepath) { + printf("gsr error: Failed to save recording\n"); + fflush(stdout); + return; + } + + puts(filepath); + fflush(stdout); + if(recording_saved_script) + run_recording_saved_script_async(recording_saved_script, filepath, "regular"); +} + +#ifdef GSR_APP_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_pipewire_audio *pipewire_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_pipewire_audio *pipewire_audio) { + 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; +#else + (void)pipewire_audio; +#endif + + 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 = (void*)arg_parser->settings.recording_saved_script; + + int error = GSR_ERROR_OK; + recorder = gsr_recorder_create(&recorder_params, &callbacks, &error); + if(!recorder) + return error; + + apply_pending_signals(recorder); + if(!running) + gsr_recorder_stop(recorder); + + const int run_result = gsr_recorder_run(recorder); + gsr_recorder_destroy(recorder); + recorder = NULL; + return run_result; +} + +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 command_exit_code = 0; + switch(args_parser_parse(&arg_parser, argc, argv, &arg_handlers, NULL, &command_exit_code)) { + case ARGS_PARSE_RESULT_ERROR: + _exit(1); + case ARGS_PARSE_RESULT_COMMAND_HANDLED: + _exit(command_exit_code); + case ARGS_PARSE_RESULT_OK: + break; + } + + 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); + } + + gsr_capture_sources capture_sources; + 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(gsr_error_to_exit_code(parse_capture_sources_result)); + + 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(1); + } + + const int validate_args_result = validate_args_with_capture_sources(&arg_parser, &capture_sources); + if(validate_args_result != GSR_ERROR_OK) + _exit(gsr_error_to_exit_code(validate_args_result)); + + gsr_audio_input_tracks audio_input_tracks; + const int parse_audio_inputs_result = parse_audio_inputs(&arg_parser, &audio_input_tracks); + if(parse_audio_inputs_result != GSR_ERROR_OK) + _exit(gsr_error_to_exit_code(parse_audio_inputs_result)); + + const bool uses_app_audio = gsr_audio_input_tracks_has_app_audio(&audio_input_tracks); + gsr_app_audio_names app_audio_names; + memset(&app_audio_names, 0, sizeof(app_audio_names)); + + gsr_pipewire_audio pipewire_audio; + memset(&pipewire_audio, 0, sizeof(pipewire_audio)); +#ifdef GSR_APP_AUDIO + if(uses_app_audio) { + const int app_audio_result = setup_app_audio(&pipewire_audio, &app_audio_names); + if(app_audio_result != GSR_ERROR_OK) + _exit(gsr_error_to_exit_code(app_audio_result)); + } +#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(2); + } +#endif + + const int validate_app_audio_result = gsr_audio_input_tracks_validate_app_audio(&audio_input_tracks, &app_audio_names); + gsr_app_audio_names_deinit(&app_audio_names); + if(validate_app_audio_result != GSR_ERROR_OK) + _exit(gsr_error_to_exit_code(validate_app_audio_result)); + + gsr_windowing windowing; + 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(1); + + 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(1); + + 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(1); + + 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(2); + } + + gsr_capture_deps capture_deps; + gsr_capture_deps_init(&capture_deps); + gsr_capture_deps_init_cursor(&capture_deps, &windowing.egl, arg_parser.settings.record_cursor); + + int result = GSR_ERROR_OK; + 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(1); + } + + result = take_screenshot(&arg_parser, &windowing, &capture_deps, &capture_sources, image_format); + } else { + result = record(&arg_parser, &windowing, &capture_deps, &capture_sources, &audio_input_tracks, &pipewire_audio); + } + + gsr_capture_deps_deinit(&capture_deps); +#ifdef GSR_APP_AUDIO + gsr_pipewire_audio_deinit(&pipewire_audio); +#endif + gsr_audio_input_tracks_deinit(&audio_input_tracks); + gsr_capture_sources_deinit(&capture_sources); + 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(gsr_error_to_exit_code(result)); +} diff --git a/src/main.cpp b/src/main.cpp deleted file mode 100644 index 46b54e2..0000000 --- a/src/main.cpp +++ /dev/null @@ -1,1158 +0,0 @@ -extern "C" { -#include "../include/ffmpeg_utils.h" -#include "../include/recorder/audio_codec.h" -#include "../include/recorder/video_codec.h" -#include "../include/recorder/codec_select.h" -#include "../include/recorder/windowing.h" -#include "../include/recorder/capture_source.h" -#include "../include/recorder/capture_setup.h" -#include "../include/recorder/audio_input.h" -#include "../include/recorder/audio_capture.h" -#include "../include/recorder/muxer.h" -#include "../include/recorder/replay_save.h" -#include "../include/recorder/screenshot.h" -#include "../include/cli/commands.h" -#include "../include/recorder/error.h" -#include "../include/capture/nvfbc.h" -#include "../include/capture/xcomposite.h" -#include "../include/capture/ximage.h" -#include "../include/capture/kms.h" -#include "../include/capture/v4l2.h" -#ifdef GSR_PORTAL -#include "../include/capture/portal.h" -#include "../include/dbus.h" -#endif -#ifdef GSR_APP_AUDIO -#include "../include/pipewire_audio.h" -#endif -#include "../include/encoder/encoder.h" -#include "../include/encoder/video/nvenc.h" -#include "../include/encoder/video/vaapi.h" -#include "../include/encoder/video/vulkan.h" -#include "../include/encoder/video/software.h" -#include "../include/codec_query/nvenc.h" -#include "../include/codec_query/vaapi.h" -#include "../include/codec_query/vulkan.h" -#include "../include/window/x11.h" -#include "../include/window/wayland.h" -#include "../include/egl.h" -#include "../include/utils.h" -#include "../include/damage.h" -#include "../include/color_conversion.h" -#include "../include/image_writer.h" -#include "../include/args_parser.h" -#include "../include/plugins.h" -#include "../kms/client/kms_client.h" -} - -#include "../include/log.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "../include/sound.h" - -extern "C" { -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -} - -#include - -#ifndef GSR_VERSION -#define GSR_VERSION "unknown" -#endif - -// TODO: If options are not supported then they are returned (allocated) in the options. This should be free'd. - -// TODO: Remove LIBAVUTIL_VERSION_MAJOR checks in the future when ubuntu, pop os LTS etc update ffmpeg to >= 5.0 - -static const int VIDEO_STREAM_INDEX = 0; - -/* AV1/VP9 use the 0-255 qindex quantizer scale and VP8 uses 0-127. Unlike h264 qp the quantizer step size - is not exponential in qindex, so the mapping to an equivalent h264 qp is not linear in the quantizer range. - qindex = h264 qp * 4 matches the qindex = crf * 4 scale used by libvpx/libaom, where crf is roughly equivalent to h264 qp. */ -static sig_atomic_t running = 1; -static sig_atomic_t toggle_pause = 0; -static sig_atomic_t toggle_replay_recording = 0; -static sig_atomic_t save_replay_seconds = 0; - -static void stop_handler(int) { - running = 0; -} - -static void toggle_pause_handler(int) { - toggle_pause = 1; -} - -static void toggle_replay_recording_handler(int) { - toggle_replay_recording = 1; -} - -static void save_replay_handler(int) { - save_replay_seconds = GSR_SAVE_REPLAY_SECONDS_FULL; -} - -static void save_replay_10_seconds_handler(int) { - save_replay_seconds = 10; -} - -static void save_replay_30_seconds_handler(int) { - save_replay_seconds = 30; -} - -static void save_replay_1_minute_handler(int) { - save_replay_seconds = 60; -} - -static void save_replay_5_minutes_handler(int) { - save_replay_seconds = 60*5; -} - -static void save_replay_10_minutes_handler(int) { - save_replay_seconds = 60*10; -} - -static void save_replay_30_minutes_handler(int) { - save_replay_seconds = 60*30; -} - -// TODO: Cleanup -// Returns the number of monitors found -// |card_path| can be NULL. If not NULL then |vendor| has to be valid -static gsr_capture_deps capture_deps; - -// TODO: 10-bit and hdr. -// Manually check if the audio inputs we give exist. This is only needed for pipewire, not pulseaudio. -// Pipewire instead DEFAULTS TO THE DEFAULT AUDIO INPUT. THAT'S RETARDED. -// OH, YOU MISSPELLED THE AUDIO INPUT? FUCK YOU -// Should use amix if more than 1 audio device and 0 application audio, merged -/* Returns -1 if none is available */ -#ifdef GSR_APP_AUDIO -#endif - -static void screenshot_saved_callback(const char *filepath, void *userdata) { - const char *recording_saved_script = (const char*)userdata; - if(recording_saved_script) - run_recording_saved_script_async(recording_saved_script, filepath, "screenshot"); -} - -static void set_display_server_environment_variables() { - // 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 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(); - _exit(1); - } - - 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(); - _exit(1); - } - } 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(); - _exit(1); - } - } - - 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"); -} - -static void install_cuda_no_stable_perf_limit() { - 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); -} - -int main(int argc, char **argv) { - setlocale(LC_ALL, "C"); // Sigh... stupid C -#ifdef __GLIBC__ - mallopt(M_MMAP_THRESHOLD, 65536); -#endif - - 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); - - set_display_server_environment_variables(); - install_cuda_no_stable_perf_limit(); - - // 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"); - - 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 command_exit_code = 0; - switch(args_parser_parse(&arg_parser, argc, argv, &arg_handlers, NULL, &command_exit_code)) { - case ARGS_PARSE_RESULT_ERROR: - _exit(1); - case ARGS_PARSE_RESULT_COMMAND_HANDLED: - _exit(command_exit_code); - case ARGS_PARSE_RESULT_OK: - break; - } - - 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); - } - - gsr_capture_sources capture_sources_data; - const int parse_capture_sources_result = gsr_capture_sources_parse(&capture_sources_data, arg_parser.settings.capture_source, arg_parser.settings.region_position, arg_parser.settings.region_size); - if(parse_capture_sources_result != GSR_ERROR_OK) - _exit(gsr_error_to_exit_code(parse_capture_sources_result)); - - gsr_capture_sources *capture_sources = &capture_sources_data; - 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(1); - } - validate_args_with_capture_sources(arg_parser, capture_sources); - - //av_log_set_level(AV_LOG_TRACE); - - 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); - - gsr_audio_input_tracks requested_audio_inputs; - const int parse_audio_inputs_result = gsr_audio_input_tracks_parse(&requested_audio_inputs, audio_input_arg->values, audio_input_arg->num_values, &audio_devices); - gsr_audio_devices_deinit(&audio_devices); - if(parse_audio_inputs_result != GSR_ERROR_OK) - _exit(gsr_error_to_exit_code(parse_audio_inputs_result)); - - const bool uses_app_audio = gsr_audio_input_tracks_has_app_audio(&requested_audio_inputs); - gsr_app_audio_names app_audio_names; - memset(&app_audio_names, 0, sizeof(app_audio_names)); -#ifdef GSR_APP_AUDIO - gsr_pipewire_audio pipewire_audio; - memset(&pipewire_audio, 0, sizeof(pipewire_audio)); - if(uses_app_audio) { - 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"); - _exit(2); - } - - if(!gsr_pipewire_audio_init(&pipewire_audio)) { - gsr_log(GSR_LOG_LEVEL_ERROR, "failed to setup PipeWire audio for application audio capture"); - _exit(2); - } - - gsr_pipewire_audio_for_each_app(&pipewire_audio, [](const char *app_name, void *userdata) { - gsr_app_audio_names *app_audio_names = (gsr_app_audio_names*)userdata; - gsr_app_audio_names_add(app_audio_names, app_name); - return true; - }, &app_audio_names); - } -#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(2); - } -#endif - - const int validate_app_audio_result = gsr_audio_input_tracks_validate_app_audio(&requested_audio_inputs, &app_audio_names); - gsr_app_audio_names_deinit(&app_audio_names); - if(validate_app_audio_result != GSR_ERROR_OK) - _exit(gsr_error_to_exit_code(validate_app_audio_result)); - - gsr_windowing windowing; - 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(1); - - Display *dpy = windowing.display; - gsr_window *window = windowing.window; - - 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(1); - - gsr_egl &egl = windowing.egl; - - 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, &egl)) - _exit(1); - - 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(2); - } - - gsr_capture_deps_init_cursor(&capture_deps, &egl, arg_parser.settings.record_cursor); - - // if(wayland && arg_parser.capture_source_type == GSR_CAPTURE_SOURCE_TYPE_MONITOR) { - // fprintf(stderr, "gsr warning: it's not possible to sync video to recorded monitor exactly on wayland when recording a monitor." - // " If you experience stutter in the video then record with portal capture option instead (-w portal) or use X11 instead\n"); - // } - - gsr_image_format image_format; - if(get_image_format_from_filename(arg_parser.settings.filename, &image_format)) { - if(audio_input_arg->num_values > 0) { - gsr_log(GSR_LOG_LEVEL_ERROR, "can't record audio (-a) when taking a screenshot"); - _exit(1); - } - - const Arg *screenshot_plugin_arg = args_parser_get_arg(&arg_parser, "-p"); - assert(screenshot_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 = &egl; - screenshot_params.window = window; - screenshot_params.capture_deps = &capture_deps; - screenshot_params.capture_sources = capture_sources; - screenshot_params.image_format = image_format; - screenshot_params.plugin_filepaths = screenshot_plugin_arg->values; - screenshot_params.num_plugin_filepaths = screenshot_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; - - const int screenshot_result = gsr_screenshot_take(&screenshot_params); - _exit(gsr_error_to_exit_code(screenshot_result)); - } - - AVFormatContext *av_format_context; - // The output format is automatically guessed by the file extension - avformat_alloc_output_context2(&av_format_context, nullptr, arg_parser.settings.container_format, arg_parser.settings.filename); - if (!av_format_context) { - if(arg_parser.settings.container_format) { - gsr_log(GSR_LOG_LEVEL_ERROR, "Container format '%s' (argument -c) is not valid", arg_parser.settings.container_format); - } else { - gsr_log(GSR_LOG_LEVEL_ERROR, "Failed to deduce container format from file extension. Use the '-c' option to specify container format"); - args_parser_print_usage(); - _exit(1); - } - _exit(1); - } - - set_format_context_options(av_format_context); - - const AVOutputFormat *output_format = av_format_context->oformat; - - std::string file_extension = output_format->extensions ? output_format->extensions : ""; - { - size_t comma_index = file_extension.find(','); - if(comma_index != std::string::npos) - file_extension = file_extension.substr(0, comma_index); - } - - if(file_extension.empty()) - file_extension = arg_parser.settings.container_format ? arg_parser.settings.container_format : ""; - - const bool force_no_audio_offset = arg_parser.settings.is_livestream || arg_parser.settings.is_output_piped || (file_extension != "mp4" && file_extension != "mkv" && file_extension != "webm"); - const double target_fps = 1.0 / (double)arg_parser.settings.fps; - - const bool uses_amix = gsr_audio_input_tracks_should_use_amix(&requested_audio_inputs); - arg_parser.settings.audio_codec = select_audio_codec_with_fallback(arg_parser.settings.audio_codec, file_extension.c_str(), uses_amix); - - vec2i video_size = {0, 0}; - gsr_video_sources video_sources_data; - const int video_sources_result = gsr_video_sources_create(&video_sources_data, &arg_parser.settings, &egl, &capture_deps, false, capture_sources, &video_size); - if(video_sources_result != GSR_ERROR_OK) - _exit(gsr_error_to_exit_code(video_sources_result)); - - gsr_video_sources *video_sources = &video_sources_data; - - // (Some?) livestreaming services require at least one audio track to work. - // If not audio is provided then create one silent audio track. - if(arg_parser.settings.is_livestream && requested_audio_inputs.num_items == 0) { - gsr_log(GSR_LOG_LEVEL_INFO, "live streaming but no audio track was added. Adding a silent audio track"); - gsr_merged_audio_inputs silent_audio_track; - memset(&silent_audio_track, 0, sizeof(silent_audio_track)); - gsr_audio_input silent_audio_input; - memset(&silent_audio_input, 0, sizeof(silent_audio_input)); - if(!gsr_merged_audio_inputs_add(&silent_audio_track, &silent_audio_input) || !gsr_audio_input_tracks_add(&requested_audio_inputs, &silent_audio_track)) - _exit(1); - } - - AVStream *video_stream = nullptr; - - if(arg_parser.settings.video_encoder == GSR_VIDEO_ENCODER_HW_CPU && arg_parser.settings.video_codec != (gsr_video_codec)GSR_VIDEO_CODEC_AUTO && arg_parser.settings.video_codec != GSR_VIDEO_CODEC_H264) { - gsr_log(GSR_LOG_LEVEL_ERROR, "-encoder cpu was specified but a codec other than h264 was specified. -encoder cpu supports only h264 at the moment"); - _exit(1); - } - - bool low_power = false; - const AVCodec *video_codec_f = nullptr; - const int select_video_codec_result = select_video_codec_with_fallback(video_size, &arg_parser.settings, file_extension.c_str(), &egl, &low_power, &video_codec_f); - if(select_video_codec_result != GSR_ERROR_OK) - _exit(gsr_error_to_exit_code(select_video_codec_result)); - - const enum AVPixelFormat video_pix_fmt = get_pixel_format(arg_parser.settings.video_codec, egl.gpu_info.vendor, arg_parser.settings.video_encoder == GSR_VIDEO_ENCODER_HW_CPU); - AVCodecContext *video_codec_context = create_video_codec_context(video_pix_fmt, video_codec_f, &egl, &arg_parser.settings, video_size.x, video_size.y); - if(!arg_parser.settings.is_replaying) - video_stream = create_stream(av_format_context, video_codec_context); - - AVFrame *video_frame = av_frame_alloc(); - if(!video_frame) { - gsr_log(GSR_LOG_LEVEL_ERROR, "Failed to allocate video frame"); - _exit(1); - } - video_frame->format = video_codec_context->pix_fmt; - video_frame->width = video_size.x; - video_frame->height = video_size.y; - video_frame->color_range = video_codec_context->color_range; - video_frame->color_primaries = video_codec_context->color_primaries; - video_frame->color_trc = video_codec_context->color_trc; - video_frame->colorspace = video_codec_context->colorspace; - video_frame->chroma_location = video_codec_context->chroma_sample_location; - - const size_t estimated_replay_buffer_packets = calculate_estimated_replay_buffer_packets(arg_parser.settings.replay_buffer_size_secs, arg_parser.settings.fps, arg_parser.settings.audio_codec, &requested_audio_inputs); - gsr_recording_clock *recording_clock = gsr_recording_clock_create(); - if(!recording_clock) - _exit(1); - - gsr_encoder encoder; - if(!gsr_encoder_init(&encoder, arg_parser.settings.replay_storage, estimated_replay_buffer_packets, arg_parser.settings.replay_buffer_size_secs, arg_parser.settings.filename)) { - gsr_log(GSR_LOG_LEVEL_ERROR, "failed to create encoder"); - _exit(1); - } - - gsr_video_encoder *video_encoder = create_video_encoder(&egl, &arg_parser.settings); - if(!video_encoder) { - gsr_log(GSR_LOG_LEVEL_ERROR, "failed to create video encoder"); - _exit(1); - } - - if(!gsr_video_encoder_start(video_encoder, video_codec_context, video_frame)) { - gsr_log(GSR_LOG_LEVEL_ERROR, "failed to start video encoder"); - _exit(1); - } - - video_size.x = video_codec_context->width; - video_size.y = video_codec_context->height; - gsr_video_sources_update_with_real_video_size(video_sources, video_size); - - const Arg *plugin_arg = args_parser_get_arg(&arg_parser, "-p"); - assert(plugin_arg); - - gsr_plugins plugins; - memset(&plugins, 0, sizeof(plugins)); - - if(gsr_load_plugins(&plugins, plugin_arg->values, plugin_arg->num_values, &arg_parser.settings, &egl, video_size) != GSR_ERROR_OK) - _exit(1); - - gsr_color_conversion_params color_conversion_params; - memset(&color_conversion_params, 0, sizeof(color_conversion_params)); - color_conversion_params.color_range = arg_parser.settings.color_range; - color_conversion_params.egl = &egl; - color_conversion_params.load_external_image_shader = gsr_video_sources_uses_external_image(video_sources); - gsr_video_encoder_get_textures(video_encoder, color_conversion_params.destination_textures, color_conversion_params.destination_textures_size, &color_conversion_params.num_destination_textures, &color_conversion_params.destination_color); - - gsr_color_conversion color_conversion; - if(gsr_color_conversion_init(&color_conversion, &color_conversion_params) != 0) { - gsr_log(GSR_LOG_LEVEL_ERROR, "main: failed to create color conversion"); - _exit(1); - } - - gsr_color_conversion_clear(&color_conversion); - - gsr_color_conversion *output_color_conversion = plugins.num_plugins > 0 ? &plugins.color_conversion : &color_conversion; - - if(arg_parser.settings.video_encoder == GSR_VIDEO_ENCODER_HW_CPU) { - if(!open_video_software(video_codec_context, &arg_parser.settings)) - _exit(1); - } else { - if(!open_video_hardware(video_codec_context, low_power, &egl, &arg_parser.settings)) - _exit(1); - } - - if(video_stream) { - avcodec_parameters_from_context(video_stream->codecpar, video_codec_context); - const size_t video_destination_id = gsr_encoder_add_recording_destination(&encoder, video_codec_context, av_format_context, video_stream, 0); - if(arg_parser.settings.write_first_frame_ts && video_destination_id != (size_t)-1) { - std::string ts_filepath = std::string(arg_parser.settings.filename) + ".ts"; - gsr_encoder_set_recording_destination_first_frame_ts_filepath(&encoder, video_destination_id, ts_filepath.c_str()); - } - } - - gsr_audio_capture audio_capture; - if(gsr_audio_capture_init(&audio_capture, &encoder, recording_clock, &running) != GSR_ERROR_OK) - _exit(1); - - int audio_max_frame_size = 1024; - int audio_stream_index = VIDEO_STREAM_INDEX + 1; - for(size_t audio_track_index = 0; audio_track_index < requested_audio_inputs.num_items; ++audio_track_index) { - const gsr_merged_audio_inputs &merged_audio_inputs = requested_audio_inputs.items[audio_track_index]; - const bool use_amix = gsr_audio_inputs_should_use_amix(&merged_audio_inputs); - AVCodecContext *audio_codec_context = create_audio_codec_context(arg_parser.settings.fps, arg_parser.settings.audio_codec, use_amix, arg_parser.settings.audio_bitrate); - if(!audio_codec_context) - _exit(1); - - AVStream *audio_stream = nullptr; - if(!arg_parser.settings.is_replaying) { - audio_stream = create_stream(av_format_context, audio_codec_context); - if(gsr_encoder_add_recording_destination(&encoder, audio_codec_context, av_format_context, audio_stream, 0) == (size_t)-1) - gsr_log(GSR_LOG_LEVEL_ERROR, "added too many audio sources"); - } - - if(audio_stream && merged_audio_inputs.track_name[0] != '\0' && !arg_parser.settings.exclude_metadata) - av_dict_set(&audio_stream->metadata, "title", merged_audio_inputs.track_name, 0); - - if(!open_audio(audio_codec_context, arg_parser.settings.ffmpeg_audio_opts)) - _exit(1); - if(audio_stream) - avcodec_parameters_from_context(audio_stream->codecpar, audio_codec_context); - - #if LIBAVCODEC_VERSION_MAJOR < 60 - const int num_channels = audio_codec_context->channels; - #else - const int num_channels = audio_codec_context->ch_layout.nb_channels; - #endif - - //audio_frame->sample_rate = audio_codec_context->sample_rate; - - AVFilterContext *src_filter_ctx[GSR_MAX_AUDIO_SOURCES_PER_TRACK]; - AVFilterGraph *graph = nullptr; - AVFilterContext *sink = nullptr; - if(use_amix) { - if(merged_audio_inputs.num_items > GSR_MAX_AUDIO_SOURCES_PER_TRACK) { - gsr_log(GSR_LOG_LEVEL_ERROR, "too many audio sources for one audio track, the maximum is %d", GSR_MAX_AUDIO_SOURCES_PER_TRACK); - _exit(1); - } - - if(gsr_audio_init_filter_graph(audio_codec_context, &graph, &sink, src_filter_ctx, merged_audio_inputs.num_items) < 0) { - gsr_log(GSR_LOG_LEVEL_ERROR, "failed to create audio filter"); - _exit(1); - } - } - - // TODO: Cleanup above - - const double audio_fps = (double)audio_codec_context->sample_rate / (double)audio_codec_context->frame_size; - const double timeout_sec = 1000.0 / audio_fps / 1000.0; - - const double audio_startup_time_seconds = force_no_audio_offset ? 0 : audio_codec_get_desired_delay(arg_parser.settings.audio_codec, arg_parser.settings.fps);// * ((double)audio_codec_context->frame_size / 1024.0); - const double num_audio_frames_shift = audio_startup_time_seconds / timeout_sec; - - gsr_audio_track audio_track; - memset(&audio_track, 0, sizeof(audio_track)); - snprintf(audio_track.name, sizeof(audio_track.name), "%s", merged_audio_inputs.track_name); - audio_track.codec_context = audio_codec_context; - audio_track.graph = graph; - audio_track.sink = sink; - audio_track.stream_index = audio_stream_index; - audio_track.pts = -audio_codec_context->frame_size * num_audio_frames_shift; - - int audio_track_result = GSR_ERROR_OK; - if(gsr_audio_inputs_has_app_audio(&merged_audio_inputs)) { - assert(!use_amix); -#ifdef GSR_APP_AUDIO - audio_track_result = gsr_audio_track_init_application_input(&audio_track, &merged_audio_inputs, audio_codec_context, num_channels, num_audio_frames_shift, &pipewire_audio); -#endif - } else { - audio_track_result = gsr_audio_track_init_device_inputs(&audio_track, &merged_audio_inputs, audio_codec_context, num_channels, num_audio_frames_shift, src_filter_ctx, use_amix); - } - - if(audio_track_result != GSR_ERROR_OK) - _exit(gsr_error_to_exit_code(audio_track_result)); - - if(!gsr_audio_capture_add_track(&audio_capture, &audio_track)) - _exit(1); - ++audio_stream_index; - - if(audio_codec_context->frame_size > audio_max_frame_size) - audio_max_frame_size = audio_codec_context->frame_size; - } - - //av_dump_format(av_format_context, 0, filename, 1); - - if(!arg_parser.settings.is_replaying && !(output_format->flags & AVFMT_NOFILE)) { - const int ret = avio_open(&av_format_context->pb, arg_parser.settings.filename, AVIO_FLAG_WRITE); - if(ret < 0) { - gsr_log(GSR_LOG_LEVEL_ERROR, "Could not open '%s': %s", arg_parser.settings.filename, gsr_av_error_to_string(ret)); - _exit(1); - } - } - - if(!arg_parser.settings.is_replaying) - av_write_header(av_format_context, arg_parser.settings.ffmpeg_opts); - - double fps_start_time = clock_get_monotonic_seconds(); - //double frame_timer_start = fps_start_time; - int fps_counter = 0; - int damage_fps_counter = 0; - - bool paused = false; - bool replay_recording = false; - gsr_recording_output replay_recording_output; - memset(&replay_recording_output, 0, sizeof(replay_recording_output)); - gsr_replay_save replay_save; - gsr_replay_save_init(&replay_save); - std::vector replay_recording_items; - std::string replay_recording_filepath; - bool force_iframe_frame = false; // Only needed for video since audio frames are always iframes - - gsr_recording_clock_start(recording_clock); - const double record_start_time = gsr_recording_clock_get_start_time(recording_clock); - - if(gsr_audio_capture_start(&audio_capture, audio_max_frame_size, uses_amix) != GSR_ERROR_OK) - _exit(1); - - // Set update_fps to 24 to test if duplicate/delayed frames cause video/audio desync or too fast/slow video. - //const double update_fps = fps + 190; - bool should_stop_error = false; - - int64_t video_pts_counter = 0; - int64_t video_prev_pts = 0; - - bool hdr_metadata_set = false; - const bool hdr = video_codec_is_hdr(arg_parser.settings.video_codec); - - bool use_damage_tracking = false; - gsr_damage damage; - memset(&damage, 0, sizeof(damage)); - if(arg_parser.settings.framerate_mode == GSR_FRAMERATE_MODE_CONTENT && gsr_capture_sources_has_damage_tracked_target(capture_sources)) { - if(gsr_window_get_display_server(window) == GSR_DISPLAY_SERVER_X11) { - gsr_damage_init(&damage, &egl, &capture_deps.x11_cursor, arg_parser.settings.record_cursor); - use_damage_tracking = true; - - for(size_t i = 0; i < capture_sources->num_items; ++i) { - const gsr_capture_source *capture_source = &capture_sources->items[i]; - switch(capture_source->type) { - case GSR_CAPTURE_SOURCE_TYPE_WINDOW: - gsr_damage_start_tracking_window(&damage, capture_source->window_id); - break; - case GSR_CAPTURE_SOURCE_TYPE_MONITOR: - case GSR_CAPTURE_SOURCE_TYPE_REGION: - // TODO: When capturing a region only track damage in that region - gsr_damage_start_tracking_monitor(&damage, capture_source->name); - break; - default: - break; - } - } - } else if(gsr_capture_sources_has_monitor_or_region(capture_sources)) { - gsr_log(GSR_LOG_LEVEL_WARNING, "\"-fm content\" has no effect on Wayland when recording a monitor. Either record a monitor on X11 or capture with desktop portal instead (-w portal)"); - } - } - - while(running) { - while(gsr_window_process_event(window)) { - if(capture_deps.x11_cursor_display && arg_parser.settings.record_cursor) - gsr_cursor_on_event(&capture_deps.x11_cursor, gsr_window_get_event_data(window)); - - gsr_damage_on_event(&damage, gsr_window_get_event_data(window)); - for(size_t video_source_index = 0; video_source_index < video_sources->num_items; ++video_source_index) { - gsr_video_source &video_source = video_sources->items[video_source_index]; - gsr_capture_on_event(video_source.capture, &egl); - } - } - - if(capture_deps.x11_cursor_display && arg_parser.settings.record_cursor) - gsr_cursor_tick(&capture_deps.x11_cursor, DefaultRootWindow(capture_deps.x11_cursor_display)); - - gsr_damage_tick(&damage); - - should_stop_error = false; - bool damaged = false; - - if(use_damage_tracking) - damaged = gsr_damage_is_damaged(&damage); - - for(size_t video_source_index = 0; video_source_index < video_sources->num_items; ++video_source_index) { - gsr_video_source &video_source = video_sources->items[video_source_index]; - gsr_capture_tick(video_source.capture); - - if(gsr_capture_should_stop(video_source.capture, &should_stop_error)) { - running = 0; - break; - } - - if(video_source.capture_source->type == GSR_CAPTURE_SOURCE_TYPE_FOCUSED_WINDOW) { - assert(video_source.capture->get_window_id); - const Window damage_target_window = video_source.capture->get_window_id(video_source.capture); - - if((int64_t)damage_target_window != video_source.capture_source->window_id) { - gsr_damage_stop_tracking_window(&damage, video_source.capture_source->window_id); - if(damage_target_window != 0) - gsr_damage_start_tracking_window(&damage, damage_target_window); - } - - video_source.capture_source->window_id = damage_target_window; - } - - if(video_source.capture->is_damaged) - damaged |= video_source.capture->is_damaged(video_source.capture); - else if(!use_damage_tracking) - damaged = true; - } - - damaged |= gsr_plugins_is_damaged(&plugins); - - // TODO: Readd wayland sync warning when removing this - if(arg_parser.settings.framerate_mode != GSR_FRAMERATE_MODE_CONTENT) - damaged = true; - - if(damaged) - ++damage_fps_counter; - - ++fps_counter; - const double time_now = clock_get_monotonic_seconds(); - //const double frame_timer_elapsed = time_now - frame_timer_start; - const double elapsed = time_now - fps_start_time; - if (elapsed >= 1.0) { - if(arg_parser.settings.verbose) { - fprintf(stderr, "update fps: %d, damage fps: %d\n", fps_counter, damage_fps_counter); - } - fps_start_time = time_now; - fps_counter = 0; - damage_fps_counter = 0; - } - - const double this_video_frame_time = gsr_recording_clock_get_time(recording_clock); - const int64_t expected_frames = std::floor((this_video_frame_time - record_start_time) / target_fps); - const int64_t num_missed_frames = expected_frames - video_pts_counter; - - if(damaged && num_missed_frames >= 1 && !paused) { - // TODO: Dont do this if no damage? - egl.glClear(0); - - gsr_damage_clear(&damage); - gsr_plugins_clear_damage(&plugins); - gsr_capture_deps_cleanup_kms_fds(&capture_deps); - - gsr_capture_deps_update_kms(&capture_deps); - - bool capture_has_synchronous_task = false; - for(size_t video_source_index = 0; video_source_index < video_sources->num_items; ++video_source_index) { - gsr_video_source &video_source = video_sources->items[video_source_index]; - if(video_source.capture->clear_damage) - video_source.capture->clear_damage(video_source.capture); - - if(video_source.capture->capture_has_synchronous_task) { - capture_has_synchronous_task = video_source.capture->capture_has_synchronous_task(video_source.capture); - if(capture_has_synchronous_task) { - paused = true; - gsr_recording_clock_set_paused(recording_clock, true); - } - } - } - - for(size_t video_source_index = 0; video_source_index < video_sources->num_items; ++video_source_index) { - gsr_video_source &video_source = video_sources->items[video_source_index]; - if(video_source.capture->pre_capture) - video_source.capture->pre_capture(video_source.capture, &video_source.metadata, output_color_conversion); - } - - if(output_color_conversion->schedule_clear) { - output_color_conversion->schedule_clear = false; - gsr_color_conversion_clear(output_color_conversion); - } - - for(size_t video_source_index = 0; video_source_index < video_sources->num_items; ++video_source_index) { - gsr_video_source &video_source = video_sources->items[video_source_index]; - gsr_capture_capture(video_source.capture, &video_source.metadata, output_color_conversion); - } - - gsr_capture_deps_cleanup_kms_fds(&capture_deps); - - if(plugins.num_plugins > 0) { - gsr_plugins_draw(&plugins); - gsr_color_conversion_draw(&color_conversion, plugins.texture, - {0, 0}, video_size, - {0, 0}, video_size, - video_size, GSR_ROT_0, GSR_FLIP_NONE, GSR_SOURCE_COLOR_RGB, false); - } - - if(capture_has_synchronous_task) { - paused = false; - gsr_recording_clock_set_paused(recording_clock, false); - } - - gsr_egl_swap_buffers(&egl); - gsr_video_encoder_copy_textures_to_frame(video_encoder, video_frame, output_color_conversion); - - for(size_t video_source_index = 0; video_source_index < video_sources->num_items; ++video_source_index) { - gsr_video_source &video_source = video_sources->items[video_source_index]; - if(hdr && !hdr_metadata_set && !arg_parser.settings.is_replaying && add_hdr_metadata_to_video_stream(video_source.capture, video_stream)) - hdr_metadata_set = true; - } - - // TODO: Check if duplicate frame can be saved just by writing it with a different pts instead of sending it again - const int num_frames_to_encode = arg_parser.settings.framerate_mode == GSR_FRAMERATE_MODE_CONSTANT ? num_missed_frames : 1; - for(int i = 0; i < num_frames_to_encode; ++i) { - if(arg_parser.settings.framerate_mode == GSR_FRAMERATE_MODE_CONSTANT) { - video_frame->pts = video_pts_counter + i; - } else { - video_frame->pts = (this_video_frame_time - record_start_time) * (double)AV_TIME_BASE; - const bool same_pts = video_frame->pts == video_prev_pts; - video_prev_pts = video_frame->pts; - if(same_pts) - continue; - } - - if(force_iframe_frame) { - video_frame->pict_type = AV_PICTURE_TYPE_I; - } - - int ret = avcodec_send_frame(video_codec_context, video_frame); - if(ret == 0) { - // TODO: Move to separate thread because this could write to network (for example when livestreaming) - gsr_encoder_receive_packets(&encoder, video_codec_context, video_frame->pts, VIDEO_STREAM_INDEX); - } else { - gsr_log(GSR_LOG_LEVEL_ERROR, "avcodec_send_frame failed, error: %s", gsr_av_error_to_string(ret)); - } - - if(force_iframe_frame) { - force_iframe_frame = false; - video_frame->pict_type = AV_PICTURE_TYPE_NONE; - } - } - - video_pts_counter += num_missed_frames; - } - - if(toggle_pause == 1 && !arg_parser.settings.is_replaying) { - paused = !paused; - gsr_recording_clock_set_paused(recording_clock, paused); - gsr_log(GSR_LOG_LEVEL_INFO, paused ? "Paused" : "Unpaused"); - toggle_pause = 0; - } - - if(toggle_replay_recording && !arg_parser.settings.replay_recording_directory) { - toggle_replay_recording = 0; - printf("gsr error: Unable to start recording since the -ro option was not specified\n"); - fflush(stdout); - } - - if(toggle_replay_recording && arg_parser.settings.replay_recording_directory) { - toggle_replay_recording = 0; - const bool new_replay_recording_state = !replay_recording; - if(new_replay_recording_state) { - gsr_audio_capture_lock_filter(&audio_capture); - replay_recording_items.clear(); - char replay_recording_filepath_buf[PATH_MAX]; - const bool filepath_created = gsr_create_new_recording_filepath_from_timestamp(replay_recording_filepath_buf, sizeof(replay_recording_filepath_buf), arg_parser.settings.replay_recording_directory, "Video", file_extension.c_str(), arg_parser.settings.date_folders); - replay_recording_filepath = filepath_created ? replay_recording_filepath_buf : ""; - if(filepath_created && gsr_recording_output_start(&replay_recording_output, replay_recording_filepath.c_str(), &arg_parser.settings, video_codec_context, &audio_capture, hdr, video_sources)) { - const size_t video_recording_destination_id = gsr_encoder_add_recording_destination(&encoder, video_codec_context, replay_recording_output.av_format_context, replay_recording_output.video_stream, video_frame->pts); - if(arg_parser.settings.write_first_frame_ts && video_recording_destination_id != (size_t)-1) { - std::string ts_filepath = replay_recording_filepath + ".ts"; - gsr_encoder_set_recording_destination_first_frame_ts_filepath(&encoder, video_recording_destination_id, ts_filepath.c_str()); - } - - if(video_recording_destination_id != (size_t)-1) - replay_recording_items.push_back(video_recording_destination_id); - - for(size_t i = 0; i < replay_recording_output.num_audio_streams; ++i) { - const gsr_recording_audio_stream *audio_stream = &replay_recording_output.audio_streams[i]; - const size_t audio_recording_destination_id = gsr_encoder_add_recording_destination(&encoder, audio_stream->audio_track->codec_context, replay_recording_output.av_format_context, audio_stream->stream, audio_stream->audio_track->pts); - if(audio_recording_destination_id != (size_t)-1) - replay_recording_items.push_back(audio_recording_destination_id); - } - - replay_recording = true; - force_iframe_frame = true; - gsr_log(GSR_LOG_LEVEL_INFO, "Started recording"); - } else { - printf("gsr error: Failed to start recording\n"); - fflush(stdout); - } - gsr_audio_capture_unlock_filter(&audio_capture); - } else if(replay_recording_output.av_format_context) { - for(size_t id : replay_recording_items) { - gsr_encoder_remove_recording_destination(&encoder, id); - } - replay_recording_items.clear(); - - if(gsr_recording_output_stop(&replay_recording_output)) { - gsr_log(GSR_LOG_LEVEL_INFO, "Stopped recording"); - puts(replay_recording_filepath.c_str()); - fflush(stdout); - if(arg_parser.settings.recording_saved_script) - run_recording_saved_script_async(arg_parser.settings.recording_saved_script, replay_recording_filepath.c_str(), "regular"); - } else { - printf("gsr error: Failed to save recording\n"); - fflush(stdout); - } - - replay_recording = false; - replay_recording_filepath.clear(); - } - } - - bool replay_save_result = false; - const char *replay_save_output_filepath = nullptr; - if(gsr_replay_save_poll(&replay_save, &replay_save_result, &replay_save_output_filepath)) { - if(replay_save_output_filepath[0] == '\0' || !replay_save_result) { - printf("gsr error: Failed to save replay\n"); - fflush(stdout); - } else { - puts(replay_save_output_filepath); - fflush(stdout); - if(arg_parser.settings.recording_saved_script) - run_recording_saved_script_async(arg_parser.settings.recording_saved_script, replay_save_output_filepath, "replay"); - } - } - - if(save_replay_seconds != 0 && !gsr_replay_save_is_running(&replay_save) && arg_parser.settings.is_replaying) { - int current_save_replay_seconds = save_replay_seconds; - if(current_save_replay_seconds > 0) - current_save_replay_seconds += arg_parser.settings.keyint; - - save_replay_seconds = 0; - const bool replay_start_result = gsr_replay_save_start(&replay_save, video_codec_context, VIDEO_STREAM_INDEX, &audio_capture, &encoder, &arg_parser.settings, file_extension.c_str(), hdr, video_sources, current_save_replay_seconds); - if(!replay_start_result) { - printf("gsr error: Failed to save replay\n"); - fflush(stdout); - } - - if(arg_parser.settings.restart_replay_on_save && current_save_replay_seconds == GSR_SAVE_REPLAY_SECONDS_FULL) { - pthread_mutex_lock(&encoder.replay_mutex); - gsr_replay_buffer_clear(encoder.replay_buffer); - pthread_mutex_unlock(&encoder.replay_mutex); - } - } - - const double time_at_frame_end = gsr_recording_clock_get_time(recording_clock); - const double time_elapsed_total = time_at_frame_end - record_start_time; - const int64_t frames_elapsed = std::floor(time_elapsed_total / target_fps); - const double time_at_next_frame = (frames_elapsed + 1) * target_fps; - double time_to_next_frame = time_at_next_frame - time_elapsed_total; - if(time_to_next_frame > target_fps) - time_to_next_frame = target_fps; - const int64_t end_num_missed_frames = frames_elapsed - video_pts_counter; - - if(time_to_next_frame > 0.0 && end_num_missed_frames <= 0) - av_usleep(time_to_next_frame * 1000.0 * 1000.0); - else { - if(paused) - av_usleep(20.0 * 1000.0); // 20 milliseconds - else if(arg_parser.settings.framerate_mode == GSR_FRAMERATE_MODE_CONTENT) - av_usleep(2.8 * 1000.0); // 2.8 milliseconds - } - } - - running = 0; - - bool final_replay_save_result = false; - const char *final_replay_save_output_filepath = nullptr; - if(gsr_replay_save_join(&replay_save, &final_replay_save_result, &final_replay_save_output_filepath)) { - if(final_replay_save_output_filepath[0] == '\0') { - // TODO: Output failed to save - } else { - puts(final_replay_save_output_filepath); - fflush(stdout); - if(arg_parser.settings.recording_saved_script) - run_recording_saved_script_async(arg_parser.settings.recording_saved_script, final_replay_save_output_filepath, "replay"); - } - } - - gsr_plugins_deinit(&plugins); - - if(replay_recording_output.av_format_context) { - for(size_t id : replay_recording_items) { - gsr_encoder_remove_recording_destination(&encoder, id); - } - replay_recording_items.clear(); - - if(gsr_recording_output_stop(&replay_recording_output)) { - gsr_log(GSR_LOG_LEVEL_INFO, "Stopped recording"); - puts(replay_recording_filepath.c_str()); - fflush(stdout); - if(arg_parser.settings.recording_saved_script) - run_recording_saved_script_async(arg_parser.settings.recording_saved_script, replay_recording_filepath.c_str(), "regular"); - } else { - printf("gsr error: Failed to save recording\n"); - fflush(stdout); - } - } - - gsr_audio_capture_join_threads(&audio_capture); - - // TODO: Replace this with start_recording_create_steams - if(!arg_parser.settings.is_replaying && av_write_trailer(av_format_context) != 0) { - //fprintf(stderr, "Failed to write trailer\n"); - } - - if(!arg_parser.settings.is_replaying && !(output_format->flags & AVFMT_NOFILE)) { - avio_close(av_format_context->pb); - avformat_free_context(av_format_context); - } - - gsr_damage_deinit(&damage); - gsr_color_conversion_deinit(&color_conversion); - gsr_video_encoder_destroy(video_encoder, video_codec_context); - gsr_encoder_deinit(&encoder); - gsr_video_sources_deinit(video_sources); -#ifdef GSR_APP_AUDIO - gsr_pipewire_audio_deinit(&pipewire_audio); -#endif - gsr_capture_deps_deinit(&capture_deps); - - if(!arg_parser.settings.is_replaying && arg_parser.settings.recording_saved_script) - run_recording_saved_script_async(arg_parser.settings.recording_saved_script, arg_parser.settings.filename, "regular"); - - if(dpy) { - // TODO: This causes a crash, why? maybe some other library dlclose xlib and that also happened to unload this??? - //XCloseDisplay(dpy); - } - - //gsr_egl_unload(&egl); - //gsr_window_destroy(&window); - - //av_frame_free(&video_frame); - 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 (cuvid library in this case) seems to be waiting for a thread that never finishes execution. - // Maybe this happens because we dont clean up all ffmpeg resources? - // TODO: Investigate this. - _exit(should_stop_error ? 3 : 0); -} diff --git a/src/recorder/recorder.c b/src/recorder/recorder.c new file mode 100644 index 0000000..27cc1ad --- /dev/null +++ b/src/recorder/recorder.c @@ -0,0 +1,826 @@ +#include "../../include/recorder/recorder.h" +#include "../../include/recorder/error.h" +#include "../../include/recorder/audio_codec.h" +#include "../../include/recorder/video_codec.h" +#include "../../include/recorder/codec_select.h" +#include "../../include/recorder/muxer.h" +#include "../../include/recorder/replay_save.h" +#include "../../include/recorder/audio_capture.h" +#include "../../include/recorder/recording_clock.h" +#include "../../include/recorder/screenshot.h" +#include "../../include/encoder/encoder.h" +#include "../../include/encoder/video/video.h" +#include "../../include/window/window.h" +#include "../../include/color_conversion.h" +#include "../../include/damage.h" +#include "../../include/cursor.h" +#include "../../include/plugins.h" +#include "../../include/utils.h" +#include "../../include/ffmpeg_utils.h" +#include "../../include/log.h" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include + +#define GSR_VIDEO_STREAM_INDEX 0 + +struct gsr_recorder { + gsr_recorder_settings settings; + gsr_recorder_callbacks callbacks; + gsr_windowing *windowing; + gsr_egl *egl; + gsr_window *window; + gsr_capture_deps *capture_deps; + gsr_capture_sources *capture_sources; + gsr_audio_input_tracks *audio_input_tracks; + + char file_extension[32]; + bool force_no_audio_offset; + double target_fps; + bool uses_amix; + bool hdr; + bool low_power; + vec2i video_size; + + AVFormatContext *av_format_context; + AVStream *video_stream; + AVCodecContext *video_codec_context; + AVFrame *video_frame; + gsr_video_sources video_sources_data; + gsr_video_sources *video_sources; + gsr_encoder encoder; + bool encoder_initialized; + gsr_video_encoder *video_encoder; + gsr_color_conversion color_conversion; + bool color_conversion_initialized; + gsr_color_conversion *output_color_conversion; + gsr_plugins plugins; + gsr_recording_clock *recording_clock; + gsr_audio_capture audio_capture; + bool audio_capture_initialized; + gsr_replay_save replay_save; + + gsr_recording_output replay_recording_output; + size_t replay_recording_items[GSR_MAX_RECORDING_DESTINATIONS]; + size_t num_replay_recording_items; + char replay_recording_filepath[PATH_MAX]; + bool replay_recording; + + volatile sig_atomic_t running; + volatile sig_atomic_t toggle_pause; + volatile sig_atomic_t toggle_replay_recording; + volatile sig_atomic_t save_replay_seconds; + bool should_stop_error; + bool force_iframe_frame; + int audio_max_frame_size; + bool use_damage_tracking; + gsr_damage damage; + const char **plugin_filepaths; + int num_plugin_filepaths; +#ifdef GSR_APP_AUDIO + gsr_pipewire_audio *pipewire_audio; +#endif +}; + +static void gsr_recorder_stop_recording(gsr_recorder *self); + +static int64_t gsr_max_int64(int64_t a, int64_t b) { + return a > b ? a : b; +} + +static int64_t gsr_min_int64(int64_t a, int64_t b) { + return a < b ? a : b; +} + +gsr_recorder* gsr_recorder_create(const gsr_recorder_params *params, const gsr_recorder_callbacks *callbacks, int *error) { + *error = GSR_ERROR_GENERIC; + + gsr_recorder *self = calloc(1, sizeof(gsr_recorder)); + if(!self) { + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_recorder_create: failed to allocate recorder"); + return NULL; + } + + self->settings = *params->settings; + if(callbacks) + self->callbacks = *callbacks; + self->windowing = params->windowing; + self->egl = ¶ms->windowing->egl; + self->window = params->windowing->window; + self->capture_deps = params->capture_deps; + self->capture_sources = params->capture_sources; + self->audio_input_tracks = params->audio_input_tracks; + self->running = 1; + self->audio_max_frame_size = 1024; + self->hdr = video_codec_is_hdr(params->settings->video_codec); + self->plugin_filepaths = params->plugin_filepaths; + self->num_plugin_filepaths = params->num_plugin_filepaths; +#ifdef GSR_APP_AUDIO + self->pipewire_audio = params->pipewire_audio; +#endif + + + // The output format is automatically guessed by the file extension + avformat_alloc_output_context2(&self->av_format_context, NULL, self->settings.container_format, self->settings.filename); + if (!self->av_format_context) { + if(self->settings.container_format) { + gsr_log(GSR_LOG_LEVEL_ERROR, "Container format '%s' (argument -c) is not valid", self->settings.container_format); + } else { + gsr_log(GSR_LOG_LEVEL_ERROR, "Failed to deduce container format from file extension. Use the '-c' option to specify container format"); + args_parser_print_usage(); + _exit(1); + } + _exit(1); + } + + set_format_context_options(self->av_format_context); + + const AVOutputFormat *output_format = self->av_format_context->oformat; + + const char *file_extensions = output_format->extensions ? output_format->extensions : ""; + const char *file_extension_end = strchr(file_extensions, ','); + if(file_extension_end) + snprintf(self->file_extension, sizeof(self->file_extension), "%.*s", (int)(file_extension_end - file_extensions), file_extensions); + else + snprintf(self->file_extension, sizeof(self->file_extension), "%s", file_extensions); + + if(self->file_extension[0] == '\0') + snprintf(self->file_extension, sizeof(self->file_extension), "%s", self->settings.container_format ? self->settings.container_format : ""); + + self->force_no_audio_offset = self->settings.is_livestream || self->settings.is_output_piped || (strcmp(self->file_extension, "mp4") != 0 && strcmp(self->file_extension, "mkv") != 0 && strcmp(self->file_extension, "webm") != 0); + self->target_fps = 1.0 / (double)self->settings.fps; + + self->uses_amix = gsr_audio_input_tracks_should_use_amix(self->audio_input_tracks); + self->settings.audio_codec = select_audio_codec_with_fallback(self->settings.audio_codec, self->file_extension, self->uses_amix); + + self->video_size = (vec2i){0, 0}; + const int video_sources_result = gsr_video_sources_create(&self->video_sources_data, &self->settings, self->egl, self->capture_deps, false, self->capture_sources, &self->video_size); + if(video_sources_result != GSR_ERROR_OK) + _exit(gsr_error_to_exit_code(video_sources_result)); + + self->video_sources = &self->video_sources_data; + + // (Some?) livestreaming services require at least one audio track to work. + // If not audio is provided then create one silent audio track. + if(self->settings.is_livestream && self->audio_input_tracks->num_items == 0) { + gsr_log(GSR_LOG_LEVEL_INFO, "live streaming but no audio track was added. Adding a silent audio track"); + gsr_merged_audio_inputs silent_audio_track; + memset(&silent_audio_track, 0, sizeof(silent_audio_track)); + gsr_audio_input silent_audio_input; + memset(&silent_audio_input, 0, sizeof(silent_audio_input)); + if(!gsr_merged_audio_inputs_add(&silent_audio_track, &silent_audio_input) || !gsr_audio_input_tracks_add(self->audio_input_tracks, &silent_audio_track)) + _exit(1); + } + + self->video_stream = NULL; + + if(self->settings.video_encoder == GSR_VIDEO_ENCODER_HW_CPU && self->settings.video_codec != (gsr_video_codec)GSR_VIDEO_CODEC_AUTO && self->settings.video_codec != GSR_VIDEO_CODEC_H264) { + gsr_log(GSR_LOG_LEVEL_ERROR, "-self->encoder cpu was specified but a codec other than h264 was specified. -self->encoder cpu supports only h264 at the moment"); + _exit(1); + } + + self->low_power = false; + const AVCodec *video_codec_f = NULL; + const int select_video_codec_result = select_video_codec_with_fallback(self->video_size, &self->settings, self->file_extension, self->egl, &self->low_power, &video_codec_f); + if(select_video_codec_result != GSR_ERROR_OK) + _exit(gsr_error_to_exit_code(select_video_codec_result)); + + const enum AVPixelFormat video_pix_fmt = get_pixel_format(self->settings.video_codec, self->egl->gpu_info.vendor, self->settings.video_encoder == GSR_VIDEO_ENCODER_HW_CPU); + self->video_codec_context = create_video_codec_context(video_pix_fmt, video_codec_f, self->egl, &self->settings, self->video_size.x, self->video_size.y); + if(!self->settings.is_replaying) + self->video_stream = create_stream(self->av_format_context, self->video_codec_context); + + self->video_frame = av_frame_alloc(); + if(!self->video_frame) { + gsr_log(GSR_LOG_LEVEL_ERROR, "Failed to allocate video frame"); + _exit(1); + } + self->video_frame->format = self->video_codec_context->pix_fmt; + self->video_frame->width = self->video_size.x; + self->video_frame->height = self->video_size.y; + self->video_frame->color_range = self->video_codec_context->color_range; + self->video_frame->color_primaries = self->video_codec_context->color_primaries; + self->video_frame->color_trc = self->video_codec_context->color_trc; + self->video_frame->colorspace = self->video_codec_context->colorspace; + self->video_frame->chroma_location = self->video_codec_context->chroma_sample_location; + + const size_t estimated_replay_buffer_packets = calculate_estimated_replay_buffer_packets(self->settings.replay_buffer_size_secs, self->settings.fps, self->settings.audio_codec, self->audio_input_tracks); + self->recording_clock = gsr_recording_clock_create(); + if(!self->recording_clock) + _exit(1); + + if(!gsr_encoder_init(&self->encoder, self->settings.replay_storage, estimated_replay_buffer_packets, self->settings.replay_buffer_size_secs, self->settings.filename)) { + gsr_log(GSR_LOG_LEVEL_ERROR, "failed to create self->encoder"); + _exit(1); + } + + self->video_encoder = create_video_encoder(self->egl, &self->settings); + if(!self->video_encoder) { + gsr_log(GSR_LOG_LEVEL_ERROR, "failed to create video self->encoder"); + _exit(1); + } + + if(!gsr_video_encoder_start(self->video_encoder, self->video_codec_context, self->video_frame)) { + gsr_log(GSR_LOG_LEVEL_ERROR, "failed to start video self->encoder"); + _exit(1); + } + + self->video_size.x = self->video_codec_context->width; + self->video_size.y = self->video_codec_context->height; + gsr_video_sources_update_with_real_video_size(self->video_sources, self->video_size); + + memset(&self->plugins, 0, sizeof(self->plugins)); + + if(gsr_load_plugins(&self->plugins, self->plugin_filepaths, self->num_plugin_filepaths, &self->settings, self->egl, self->video_size) != GSR_ERROR_OK) + _exit(1); + + gsr_color_conversion_params color_conversion_params; + memset(&color_conversion_params, 0, sizeof(color_conversion_params)); + color_conversion_params.color_range = self->settings.color_range; + color_conversion_params.egl = self->egl; + color_conversion_params.load_external_image_shader = gsr_video_sources_uses_external_image(self->video_sources); + gsr_video_encoder_get_textures(self->video_encoder, color_conversion_params.destination_textures, color_conversion_params.destination_textures_size, &color_conversion_params.num_destination_textures, &color_conversion_params.destination_color); + + if(gsr_color_conversion_init(&self->color_conversion, &color_conversion_params) != 0) { + gsr_log(GSR_LOG_LEVEL_ERROR, "main: failed to create color conversion"); + _exit(1); + } + + gsr_color_conversion_clear(&self->color_conversion); + + self->output_color_conversion = self->plugins.num_plugins > 0 ? &self->plugins.color_conversion : &self->color_conversion; + + if(self->settings.video_encoder == GSR_VIDEO_ENCODER_HW_CPU) { + if(!open_video_software(self->video_codec_context, &self->settings)) + _exit(1); + } else { + if(!open_video_hardware(self->video_codec_context, self->low_power, self->egl, &self->settings)) + _exit(1); + } + + if(self->video_stream) { + avcodec_parameters_from_context(self->video_stream->codecpar, self->video_codec_context); + const size_t video_destination_id = gsr_encoder_add_recording_destination(&self->encoder, self->video_codec_context, self->av_format_context, self->video_stream, 0); + if(self->settings.write_first_frame_ts && video_destination_id != (size_t)-1) { + char ts_filepath[PATH_MAX]; + snprintf(ts_filepath, sizeof(ts_filepath), "%s.ts", self->settings.filename); + gsr_encoder_set_recording_destination_first_frame_ts_filepath(&self->encoder, video_destination_id, ts_filepath); + } + } + + if(gsr_audio_capture_init(&self->audio_capture, &self->encoder, self->recording_clock, &self->running) != GSR_ERROR_OK) + _exit(1); + + + int audio_stream_index = GSR_VIDEO_STREAM_INDEX + 1; + for(size_t audio_track_index = 0; audio_track_index < self->audio_input_tracks->num_items; ++audio_track_index) { + const gsr_merged_audio_inputs *merged_audio_inputs = &self->audio_input_tracks->items[audio_track_index]; + const bool use_amix = gsr_audio_inputs_should_use_amix(merged_audio_inputs); + AVCodecContext *audio_codec_context = create_audio_codec_context(self->settings.fps, self->settings.audio_codec, use_amix, self->settings.audio_bitrate); + if(!audio_codec_context) + _exit(1); + + AVStream *audio_stream = NULL; + if(!self->settings.is_replaying) { + audio_stream = create_stream(self->av_format_context, audio_codec_context); + if(gsr_encoder_add_recording_destination(&self->encoder, audio_codec_context, self->av_format_context, audio_stream, 0) == (size_t)-1) + gsr_log(GSR_LOG_LEVEL_ERROR, "added too many audio sources"); + } + + if(audio_stream && merged_audio_inputs->track_name[0] != '\0' && !self->settings.exclude_metadata) + av_dict_set(&audio_stream->metadata, "title", merged_audio_inputs->track_name, 0); + + if(!open_audio(audio_codec_context, self->settings.ffmpeg_audio_opts)) + _exit(1); + if(audio_stream) + avcodec_parameters_from_context(audio_stream->codecpar, audio_codec_context); + + #if LIBAVCODEC_VERSION_MAJOR < 60 + const int num_channels = audio_codec_context->channels; + #else + const int num_channels = audio_codec_context->ch_layout.nb_channels; + #endif + + //audio_frame->sample_rate = audio_codec_context->sample_rate; + + AVFilterContext *src_filter_ctx[GSR_MAX_AUDIO_SOURCES_PER_TRACK]; + AVFilterGraph *graph = NULL; + AVFilterContext *sink = NULL; + if(use_amix) { + if(merged_audio_inputs->num_items > GSR_MAX_AUDIO_SOURCES_PER_TRACK) { + gsr_log(GSR_LOG_LEVEL_ERROR, "too many audio sources for one audio track, the maximum is %d", GSR_MAX_AUDIO_SOURCES_PER_TRACK); + _exit(1); + } + + if(gsr_audio_init_filter_graph(audio_codec_context, &graph, &sink, src_filter_ctx, merged_audio_inputs->num_items) < 0) { + gsr_log(GSR_LOG_LEVEL_ERROR, "failed to create audio filter"); + _exit(1); + } + } + + // TODO: Cleanup above + + const double audio_fps = (double)audio_codec_context->sample_rate / (double)audio_codec_context->frame_size; + const double timeout_sec = 1000.0 / audio_fps / 1000.0; + + const double audio_startup_time_seconds = self->force_no_audio_offset ? 0 : audio_codec_get_desired_delay(self->settings.audio_codec, self->settings.fps);// * ((double)audio_codec_context->frame_size / 1024.0); + const double num_audio_frames_shift = audio_startup_time_seconds / timeout_sec; + + gsr_audio_track audio_track; + memset(&audio_track, 0, sizeof(audio_track)); + snprintf(audio_track.name, sizeof(audio_track.name), "%s", merged_audio_inputs->track_name); + audio_track.codec_context = audio_codec_context; + audio_track.graph = graph; + audio_track.sink = sink; + audio_track.stream_index = audio_stream_index; + audio_track.pts = -audio_codec_context->frame_size * num_audio_frames_shift; + + int audio_track_result = GSR_ERROR_OK; + if(gsr_audio_inputs_has_app_audio(merged_audio_inputs)) { + assert(!use_amix); +#ifdef GSR_APP_AUDIO + audio_track_result = gsr_audio_track_init_application_input(&audio_track, merged_audio_inputs, audio_codec_context, num_channels, num_audio_frames_shift, self->pipewire_audio); +#endif + } else { + audio_track_result = gsr_audio_track_init_device_inputs(&audio_track, merged_audio_inputs, audio_codec_context, num_channels, num_audio_frames_shift, src_filter_ctx, use_amix); + } + + if(audio_track_result != GSR_ERROR_OK) + _exit(gsr_error_to_exit_code(audio_track_result)); + + if(!gsr_audio_capture_add_track(&self->audio_capture, &audio_track)) + _exit(1); + ++audio_stream_index; + + if(audio_codec_context->frame_size > self->audio_max_frame_size) + self->audio_max_frame_size = audio_codec_context->frame_size; + } + + //av_dump_format(self->av_format_context, 0, filename, 1); + + if(!self->settings.is_replaying && !(self->av_format_context->oformat->flags & AVFMT_NOFILE)) { + const int ret = avio_open(&self->av_format_context->pb, self->settings.filename, AVIO_FLAG_WRITE); + if(ret < 0) { + gsr_log(GSR_LOG_LEVEL_ERROR, "Could not open '%s': %s", self->settings.filename, gsr_av_error_to_string(ret)); + _exit(1); + } + } + + if(!self->settings.is_replaying) + av_write_header(self->av_format_context, self->settings.ffmpeg_opts); + + *error = GSR_ERROR_OK; + return self; +} + +int gsr_recorder_run(gsr_recorder *self) { + double fps_start_time = clock_get_monotonic_seconds(); + //double frame_timer_start = fps_start_time; + int fps_counter = 0; + int damage_fps_counter = 0; + + bool paused = false; + self->replay_recording = false; + + memset(&self->replay_recording_output, 0, sizeof(self->replay_recording_output)); + + gsr_replay_save_init(&self->replay_save); + + + self->force_iframe_frame = false; + + gsr_recording_clock_start(self->recording_clock); + const double record_start_time = gsr_recording_clock_get_start_time(self->recording_clock); + + if(gsr_audio_capture_start(&self->audio_capture, self->audio_max_frame_size, self->uses_amix) != GSR_ERROR_OK) + _exit(1); + + // Set update_fps to 24 to test if duplicate/delayed frames cause video/audio desync or too fast/slow video. + //const double update_fps = fps + 190; + self->should_stop_error = false; + + int64_t video_pts_counter = 0; + int64_t video_prev_pts = 0; + + bool hdr_metadata_set = false; + self->hdr = video_codec_is_hdr(self->settings.video_codec); + + + memset(&self->damage, 0, sizeof(self->damage)); + if(self->settings.framerate_mode == GSR_FRAMERATE_MODE_CONTENT && gsr_capture_sources_has_damage_tracked_target(self->capture_sources)) { + if(gsr_window_get_display_server(self->window) == GSR_DISPLAY_SERVER_X11) { + gsr_damage_init(&self->damage, self->egl, &self->capture_deps->x11_cursor, self->settings.record_cursor); + self->use_damage_tracking = true; + + for(size_t i = 0; i < self->capture_sources->num_items; ++i) { + const gsr_capture_source *capture_source = &self->capture_sources->items[i]; + switch(capture_source->type) { + case GSR_CAPTURE_SOURCE_TYPE_WINDOW: + gsr_damage_start_tracking_window(&self->damage, capture_source->window_id); + break; + case GSR_CAPTURE_SOURCE_TYPE_MONITOR: + case GSR_CAPTURE_SOURCE_TYPE_REGION: + // TODO: When capturing a region only track damage in that region + gsr_damage_start_tracking_monitor(&self->damage, capture_source->name); + break; + default: + break; + } + } + } else if(gsr_capture_sources_has_monitor_or_region(self->capture_sources)) { + gsr_log(GSR_LOG_LEVEL_WARNING, "\"-fm content\" has no effect on Wayland when recording a monitor. Either record a monitor on X11 or capture with desktop portal instead (-w portal)"); + } + } + + while(self->running) { + while(gsr_window_process_event(self->window)) { + if(self->capture_deps->x11_cursor_display && self->settings.record_cursor) + gsr_cursor_on_event(&self->capture_deps->x11_cursor, gsr_window_get_event_data(self->window)); + + gsr_damage_on_event(&self->damage, gsr_window_get_event_data(self->window)); + for(size_t video_source_index = 0; video_source_index < self->video_sources->num_items; ++video_source_index) { + gsr_video_source *video_source = &self->video_sources->items[video_source_index]; + gsr_capture_on_event(video_source->capture, self->egl); + } + } + + if(self->capture_deps->x11_cursor_display && self->settings.record_cursor) + gsr_cursor_tick(&self->capture_deps->x11_cursor, DefaultRootWindow(self->capture_deps->x11_cursor_display)); + + gsr_damage_tick(&self->damage); + + self->should_stop_error = false; + bool damaged = false; + + if(self->use_damage_tracking) + damaged = gsr_damage_is_damaged(&self->damage); + + for(size_t video_source_index = 0; video_source_index < self->video_sources->num_items; ++video_source_index) { + gsr_video_source *video_source = &self->video_sources->items[video_source_index]; + gsr_capture_tick(video_source->capture); + + if(gsr_capture_should_stop(video_source->capture, &self->should_stop_error)) { + self->running = 0; + break; + } + + if(video_source->capture_source->type == GSR_CAPTURE_SOURCE_TYPE_FOCUSED_WINDOW) { + assert(video_source->capture->get_window_id); + const Window damage_target_window = video_source->capture->get_window_id(video_source->capture); + + if((int64_t)damage_target_window != video_source->capture_source->window_id) { + gsr_damage_stop_tracking_window(&self->damage, video_source->capture_source->window_id); + if(damage_target_window != 0) + gsr_damage_start_tracking_window(&self->damage, damage_target_window); + } + + video_source->capture_source->window_id = damage_target_window; + } + + if(video_source->capture->is_damaged) + damaged |= video_source->capture->is_damaged(video_source->capture); + else if(!self->use_damage_tracking) + damaged = true; + } + + damaged |= gsr_plugins_is_damaged(&self->plugins); + + // TODO: Readd wayland sync warning when removing this + if(self->settings.framerate_mode != GSR_FRAMERATE_MODE_CONTENT) + damaged = true; + + if(damaged) + ++damage_fps_counter; + + ++fps_counter; + const double time_now = clock_get_monotonic_seconds(); + //const double frame_timer_elapsed = time_now - frame_timer_start; + const double elapsed = time_now - fps_start_time; + if (elapsed >= 1.0) { + if(self->settings.verbose) { + fprintf(stderr, "update fps: %d, damage fps: %d\n", fps_counter, damage_fps_counter); + } + fps_start_time = time_now; + fps_counter = 0; + damage_fps_counter = 0; + } + + const double this_video_frame_time = gsr_recording_clock_get_time(self->recording_clock); + const int64_t expected_frames = floor((this_video_frame_time - record_start_time) / self->target_fps); + const int64_t num_missed_frames = expected_frames - video_pts_counter; + + if(damaged && num_missed_frames >= 1 && !paused) { + // TODO: Dont do this if no damage? + self->egl->glClear(0); + + gsr_damage_clear(&self->damage); + gsr_plugins_clear_damage(&self->plugins); + gsr_capture_deps_cleanup_kms_fds(self->capture_deps); + + gsr_capture_deps_update_kms(self->capture_deps); + + bool capture_has_synchronous_task = false; + for(size_t video_source_index = 0; video_source_index < self->video_sources->num_items; ++video_source_index) { + gsr_video_source *video_source = &self->video_sources->items[video_source_index]; + if(video_source->capture->clear_damage) + video_source->capture->clear_damage(video_source->capture); + + if(video_source->capture->capture_has_synchronous_task) { + capture_has_synchronous_task = video_source->capture->capture_has_synchronous_task(video_source->capture); + if(capture_has_synchronous_task) { + paused = true; + gsr_recording_clock_set_paused(self->recording_clock, true); + } + } + } + + for(size_t video_source_index = 0; video_source_index < self->video_sources->num_items; ++video_source_index) { + gsr_video_source *video_source = &self->video_sources->items[video_source_index]; + if(video_source->capture->pre_capture) + video_source->capture->pre_capture(video_source->capture, &video_source->metadata, self->output_color_conversion); + } + + if(self->output_color_conversion->schedule_clear) { + self->output_color_conversion->schedule_clear = false; + gsr_color_conversion_clear(self->output_color_conversion); + } + + for(size_t video_source_index = 0; video_source_index < self->video_sources->num_items; ++video_source_index) { + gsr_video_source *video_source = &self->video_sources->items[video_source_index]; + gsr_capture_capture(video_source->capture, &video_source->metadata, self->output_color_conversion); + } + + gsr_capture_deps_cleanup_kms_fds(self->capture_deps); + + if(self->plugins.num_plugins > 0) { + gsr_plugins_draw(&self->plugins); + gsr_color_conversion_draw(&self->color_conversion, self->plugins.texture, + (vec2i){0, 0}, self->video_size, + (vec2i){0, 0}, self->video_size, + self->video_size, GSR_ROT_0, GSR_FLIP_NONE, GSR_SOURCE_COLOR_RGB, false); + } + + if(capture_has_synchronous_task) { + paused = false; + gsr_recording_clock_set_paused(self->recording_clock, false); + } + + gsr_egl_swap_buffers(self->egl); + gsr_video_encoder_copy_textures_to_frame(self->video_encoder, self->video_frame, self->output_color_conversion); + + for(size_t video_source_index = 0; video_source_index < self->video_sources->num_items; ++video_source_index) { + gsr_video_source *video_source = &self->video_sources->items[video_source_index]; + if(self->hdr && !hdr_metadata_set && !self->settings.is_replaying && add_hdr_metadata_to_video_stream(video_source->capture, self->video_stream)) + hdr_metadata_set = true; + } + + // TODO: Check if duplicate frame can be saved just by writing it with a different pts instead of sending it again + const int num_frames_to_encode = self->settings.framerate_mode == GSR_FRAMERATE_MODE_CONSTANT ? num_missed_frames : 1; + for(int i = 0; i < num_frames_to_encode; ++i) { + if(self->settings.framerate_mode == GSR_FRAMERATE_MODE_CONSTANT) { + self->video_frame->pts = video_pts_counter + i; + } else { + self->video_frame->pts = (this_video_frame_time - record_start_time) * (double)AV_TIME_BASE; + const bool same_pts = self->video_frame->pts == video_prev_pts; + video_prev_pts = self->video_frame->pts; + if(same_pts) + continue; + } + + if(self->force_iframe_frame) { + self->video_frame->pict_type = AV_PICTURE_TYPE_I; + } + + int ret = avcodec_send_frame(self->video_codec_context, self->video_frame); + if(ret == 0) { + // TODO: Move to separate thread because this could write to network (for example when livestreaming) + gsr_encoder_receive_packets(&self->encoder, self->video_codec_context, self->video_frame->pts, GSR_VIDEO_STREAM_INDEX); + } else { + gsr_log(GSR_LOG_LEVEL_ERROR, "avcodec_send_frame failed, error: %s", gsr_av_error_to_string(ret)); + } + + if(self->force_iframe_frame) { + self->force_iframe_frame = false; + self->video_frame->pict_type = AV_PICTURE_TYPE_NONE; + } + } + + video_pts_counter += num_missed_frames; + } + + if(self->toggle_pause == 1 && !self->settings.is_replaying) { + paused = !paused; + gsr_recording_clock_set_paused(self->recording_clock, paused); + gsr_log(GSR_LOG_LEVEL_INFO, paused ? "Paused" : "Unpaused"); + self->toggle_pause = 0; + } + + if(self->toggle_replay_recording && !self->settings.replay_recording_directory) { + self->toggle_replay_recording = 0; + if(self->callbacks.recording_started) + self->callbacks.recording_started(NULL, self->callbacks.userdata); + } + + if(self->toggle_replay_recording && self->settings.replay_recording_directory) { + self->toggle_replay_recording = 0; + const bool new_replay_recording_state = !self->replay_recording; + if(new_replay_recording_state) { + gsr_audio_capture_lock_filter(&self->audio_capture); + self->num_replay_recording_items = 0; + const bool filepath_created = gsr_create_new_recording_filepath_from_timestamp(self->replay_recording_filepath, sizeof(self->replay_recording_filepath), self->settings.replay_recording_directory, "Video", self->file_extension, self->settings.date_folders); + if(filepath_created && gsr_recording_output_start(&self->replay_recording_output, self->replay_recording_filepath, &self->settings, self->video_codec_context, &self->audio_capture, self->hdr, self->video_sources)) { + const size_t video_recording_destination_id = gsr_encoder_add_recording_destination(&self->encoder, self->video_codec_context, self->replay_recording_output.av_format_context, self->replay_recording_output.video_stream, self->video_frame->pts); + if(self->settings.write_first_frame_ts && video_recording_destination_id != (size_t)-1) { + char ts_filepath[PATH_MAX]; + snprintf(ts_filepath, sizeof(ts_filepath), "%s.ts", self->replay_recording_filepath); + gsr_encoder_set_recording_destination_first_frame_ts_filepath(&self->encoder, video_recording_destination_id, ts_filepath); + } + + if(video_recording_destination_id != (size_t)-1 && self->num_replay_recording_items < GSR_MAX_RECORDING_DESTINATIONS) { + self->replay_recording_items[self->num_replay_recording_items] = video_recording_destination_id; + ++self->num_replay_recording_items; + } + + for(size_t i = 0; i < self->replay_recording_output.num_audio_streams; ++i) { + const gsr_recording_audio_stream *audio_stream = &self->replay_recording_output.audio_streams[i]; + const size_t audio_recording_destination_id = gsr_encoder_add_recording_destination(&self->encoder, audio_stream->audio_track->codec_context, self->replay_recording_output.av_format_context, audio_stream->stream, audio_stream->audio_track->pts); + if(audio_recording_destination_id != (size_t)-1 && self->num_replay_recording_items < GSR_MAX_RECORDING_DESTINATIONS) { + self->replay_recording_items[self->num_replay_recording_items] = audio_recording_destination_id; + ++self->num_replay_recording_items; + } + } + + self->replay_recording = true; + self->force_iframe_frame = true; + gsr_log(GSR_LOG_LEVEL_INFO, "Started recording"); + if(self->callbacks.recording_started) + self->callbacks.recording_started(self->replay_recording_filepath, self->callbacks.userdata); + } else { + if(self->callbacks.recording_started) + self->callbacks.recording_started(NULL, self->callbacks.userdata); + } + gsr_audio_capture_unlock_filter(&self->audio_capture); + } else if(self->replay_recording_output.av_format_context) { + for(size_t i = 0; i < self->num_replay_recording_items; ++i) { + gsr_encoder_remove_recording_destination(&self->encoder, self->replay_recording_items[i]); + } + self->num_replay_recording_items = 0; + + if(gsr_recording_output_stop(&self->replay_recording_output)) { + gsr_log(GSR_LOG_LEVEL_INFO, "Stopped recording"); + if(self->callbacks.recording_stopped) + self->callbacks.recording_stopped(self->replay_recording_filepath, self->callbacks.userdata); + } else { + if(self->callbacks.recording_stopped) + self->callbacks.recording_stopped(NULL, self->callbacks.userdata); + } + + self->replay_recording = false; + self->replay_recording_filepath[0] = '\0'; + } + } + + bool replay_save_result = false; + const char *replay_save_output_filepath = NULL; + if(gsr_replay_save_poll(&self->replay_save, &replay_save_result, &replay_save_output_filepath)) { + if(self->callbacks.replay_saved) + self->callbacks.replay_saved(replay_save_output_filepath[0] == '\0' || !replay_save_result ? NULL : replay_save_output_filepath, self->callbacks.userdata); + } + + if(self->save_replay_seconds != 0 && !gsr_replay_save_is_running(&self->replay_save) && self->settings.is_replaying) { + int current_save_replay_seconds = self->save_replay_seconds; + if(current_save_replay_seconds > 0) + current_save_replay_seconds += self->settings.keyint; + + self->save_replay_seconds = 0; + const bool replay_start_result = gsr_replay_save_start(&self->replay_save, self->video_codec_context, GSR_VIDEO_STREAM_INDEX, &self->audio_capture, &self->encoder, &self->settings, self->file_extension, self->hdr, self->video_sources, current_save_replay_seconds); + if(!replay_start_result && self->callbacks.replay_saved) + self->callbacks.replay_saved(NULL, self->callbacks.userdata); + + if(self->settings.restart_replay_on_save && current_save_replay_seconds == GSR_SAVE_REPLAY_SECONDS_FULL) { + pthread_mutex_lock(&self->encoder.replay_mutex); + gsr_replay_buffer_clear(self->encoder.replay_buffer); + pthread_mutex_unlock(&self->encoder.replay_mutex); + } + } + + const double time_at_frame_end = gsr_recording_clock_get_time(self->recording_clock); + const double time_elapsed_total = time_at_frame_end - record_start_time; + const int64_t frames_elapsed = floor(time_elapsed_total / self->target_fps); + const double time_at_next_frame = (frames_elapsed + 1) * self->target_fps; + double time_to_next_frame = time_at_next_frame - time_elapsed_total; + if(time_to_next_frame > self->target_fps) + time_to_next_frame = self->target_fps; + const int64_t end_num_missed_frames = frames_elapsed - video_pts_counter; + + if(time_to_next_frame > 0.0 && end_num_missed_frames <= 0) + av_usleep(time_to_next_frame * 1000.0 * 1000.0); + else { + if(paused) + av_usleep(20.0 * 1000.0); // 20 milliseconds + else if(self->settings.framerate_mode == GSR_FRAMERATE_MODE_CONTENT) + av_usleep(2.8 * 1000.0); // 2.8 milliseconds + } + } + + gsr_recorder_stop_recording(self); + return self->should_stop_error ? GSR_ERROR_CAPTURE_FAILED : GSR_ERROR_OK; +} + +static void gsr_recorder_stop_recording(gsr_recorder *self) { + self->running = 0; + + bool final_replay_save_result = false; + const char *final_replay_save_output_filepath = NULL; + if(gsr_replay_save_join(&self->replay_save, &final_replay_save_result, &final_replay_save_output_filepath)) { + if(final_replay_save_output_filepath[0] != '\0' && self->callbacks.replay_saved) + self->callbacks.replay_saved(final_replay_save_output_filepath, self->callbacks.userdata); + } + + gsr_plugins_deinit(&self->plugins); + + if(self->replay_recording_output.av_format_context) { + for(size_t i = 0; i < self->num_replay_recording_items; ++i) { + gsr_encoder_remove_recording_destination(&self->encoder, self->replay_recording_items[i]); + } + self->num_replay_recording_items = 0; + + if(gsr_recording_output_stop(&self->replay_recording_output)) { + gsr_log(GSR_LOG_LEVEL_INFO, "Stopped recording"); + if(self->callbacks.recording_stopped) + self->callbacks.recording_stopped(self->replay_recording_filepath, self->callbacks.userdata); + } else { + if(self->callbacks.recording_stopped) + self->callbacks.recording_stopped(NULL, self->callbacks.userdata); + } + } + + gsr_audio_capture_join_threads(&self->audio_capture); + + // TODO: Replace this with start_recording_create_steams + if(!self->settings.is_replaying && av_write_trailer(self->av_format_context) != 0) { + //fprintf(stderr, "Failed to write trailer\n"); + } + + if(!self->settings.is_replaying && !(self->av_format_context->oformat->flags & AVFMT_NOFILE)) { + avio_close(self->av_format_context->pb); + avformat_free_context(self->av_format_context); + } + + gsr_damage_deinit(&self->damage); + gsr_color_conversion_deinit(&self->color_conversion); + gsr_video_encoder_destroy(self->video_encoder, self->video_codec_context); + gsr_encoder_deinit(&self->encoder); + gsr_video_sources_deinit(self->video_sources); +#ifdef GSR_APP_AUDIO + gsr_pipewire_audio_deinit(self->pipewire_audio); +#endif + gsr_capture_deps_deinit(self->capture_deps); + + if(!self->settings.is_replaying && self->callbacks.recording_stopped) + self->callbacks.recording_stopped(self->settings.filename, self->callbacks.userdata); + + if(self->windowing->display) { + // TODO: This causes a crash, why? maybe some other library dlclose xlib and that also happened to unload this??? + //XCloseDisplay(dpy); + } + + //gsr_egl_unload(self->egl); + //gsr_window_destroy(&window); + + //av_frame_free(&self->video_frame); +} + +void gsr_recorder_destroy(gsr_recorder *self) { + if(!self) + return; + + gsr_recording_clock_destroy(self->recording_clock); + free(self); +} + +void gsr_recorder_stop(gsr_recorder *self) { + self->running = 0; +} + +void gsr_recorder_toggle_pause(gsr_recorder *self) { + self->toggle_pause = 1; +} + +void gsr_recorder_toggle_replay_recording(gsr_recorder *self) { + self->toggle_replay_recording = 1; +} + +void gsr_recorder_save_replay(gsr_recorder *self, int seconds) { + self->save_replay_seconds = seconds; +} -- cgit v1.2.3 From 4d3a8cb421c9d64c1a8ec86eac0c55b4b4053664 Mon Sep 17 00:00:00 2001 From: dec05eba Date: Sat, 1 Aug 2026 23:28:16 +0200 Subject: Return errors from gsr_recorder_create instead of exiting The recorder now cleans up after a failed setup and the caller decides what to do with the error, so only main and forked child processes exit. --- src/recorder/recorder.c | 196 +++++++++++++++++++++++++++++------------------- 1 file changed, 119 insertions(+), 77 deletions(-) diff --git a/src/recorder/recorder.c b/src/recorder/recorder.c index 27cc1ad..9eb819f 100644 --- a/src/recorder/recorder.c +++ b/src/recorder/recorder.c @@ -94,14 +94,6 @@ struct gsr_recorder { static void gsr_recorder_stop_recording(gsr_recorder *self); -static int64_t gsr_max_int64(int64_t a, int64_t b) { - return a > b ? a : b; -} - -static int64_t gsr_min_int64(int64_t a, int64_t b) { - return a < b ? a : b; -} - gsr_recorder* gsr_recorder_create(const gsr_recorder_params *params, const gsr_recorder_callbacks *callbacks, int *error) { *error = GSR_ERROR_GENERIC; @@ -122,6 +114,7 @@ gsr_recorder* gsr_recorder_create(const gsr_recorder_params *params, const gsr_r self->audio_input_tracks = params->audio_input_tracks; self->running = 1; self->audio_max_frame_size = 1024; + int error_code = GSR_ERROR_GENERIC; self->hdr = video_codec_is_hdr(params->settings->video_codec); self->plugin_filepaths = params->plugin_filepaths; self->num_plugin_filepaths = params->num_plugin_filepaths; @@ -129,7 +122,6 @@ gsr_recorder* gsr_recorder_create(const gsr_recorder_params *params, const gsr_r self->pipewire_audio = params->pipewire_audio; #endif - // The output format is automatically guessed by the file extension avformat_alloc_output_context2(&self->av_format_context, NULL, self->settings.container_format, self->settings.filename); if (!self->av_format_context) { @@ -138,9 +130,11 @@ gsr_recorder* gsr_recorder_create(const gsr_recorder_params *params, const gsr_r } else { gsr_log(GSR_LOG_LEVEL_ERROR, "Failed to deduce container format from file extension. Use the '-c' option to specify container format"); args_parser_print_usage(); - _exit(1); + error_code = GSR_ERROR_GENERIC; + goto fail; } - _exit(1); + error_code = GSR_ERROR_GENERIC; + goto fail; } set_format_context_options(self->av_format_context); @@ -165,8 +159,10 @@ gsr_recorder* gsr_recorder_create(const gsr_recorder_params *params, const gsr_r self->video_size = (vec2i){0, 0}; const int video_sources_result = gsr_video_sources_create(&self->video_sources_data, &self->settings, self->egl, self->capture_deps, false, self->capture_sources, &self->video_size); - if(video_sources_result != GSR_ERROR_OK) - _exit(gsr_error_to_exit_code(video_sources_result)); + if(video_sources_result != GSR_ERROR_OK) { + error_code = video_sources_result; + goto fail; + } self->video_sources = &self->video_sources_data; @@ -178,22 +174,27 @@ gsr_recorder* gsr_recorder_create(const gsr_recorder_params *params, const gsr_r memset(&silent_audio_track, 0, sizeof(silent_audio_track)); gsr_audio_input silent_audio_input; memset(&silent_audio_input, 0, sizeof(silent_audio_input)); - if(!gsr_merged_audio_inputs_add(&silent_audio_track, &silent_audio_input) || !gsr_audio_input_tracks_add(self->audio_input_tracks, &silent_audio_track)) - _exit(1); + if(!gsr_merged_audio_inputs_add(&silent_audio_track, &silent_audio_input) || !gsr_audio_input_tracks_add(self->audio_input_tracks, &silent_audio_track)) { + error_code = GSR_ERROR_GENERIC; + goto fail; + } } self->video_stream = NULL; if(self->settings.video_encoder == GSR_VIDEO_ENCODER_HW_CPU && self->settings.video_codec != (gsr_video_codec)GSR_VIDEO_CODEC_AUTO && self->settings.video_codec != GSR_VIDEO_CODEC_H264) { - gsr_log(GSR_LOG_LEVEL_ERROR, "-self->encoder cpu was specified but a codec other than h264 was specified. -self->encoder cpu supports only h264 at the moment"); - _exit(1); + gsr_log(GSR_LOG_LEVEL_ERROR, "-encoder cpu was specified but a codec other than h264 was specified. -encoder cpu supports only h264 at the moment"); + error_code = GSR_ERROR_GENERIC; + goto fail; } self->low_power = false; const AVCodec *video_codec_f = NULL; const int select_video_codec_result = select_video_codec_with_fallback(self->video_size, &self->settings, self->file_extension, self->egl, &self->low_power, &video_codec_f); - if(select_video_codec_result != GSR_ERROR_OK) - _exit(gsr_error_to_exit_code(select_video_codec_result)); + if(select_video_codec_result != GSR_ERROR_OK) { + error_code = select_video_codec_result; + goto fail; + } const enum AVPixelFormat video_pix_fmt = get_pixel_format(self->settings.video_codec, self->egl->gpu_info.vendor, self->settings.video_encoder == GSR_VIDEO_ENCODER_HW_CPU); self->video_codec_context = create_video_codec_context(video_pix_fmt, video_codec_f, self->egl, &self->settings, self->video_size.x, self->video_size.y); @@ -203,7 +204,8 @@ gsr_recorder* gsr_recorder_create(const gsr_recorder_params *params, const gsr_r self->video_frame = av_frame_alloc(); if(!self->video_frame) { gsr_log(GSR_LOG_LEVEL_ERROR, "Failed to allocate video frame"); - _exit(1); + error_code = GSR_ERROR_GENERIC; + goto fail; } self->video_frame->format = self->video_codec_context->pix_fmt; self->video_frame->width = self->video_size.x; @@ -216,23 +218,29 @@ gsr_recorder* gsr_recorder_create(const gsr_recorder_params *params, const gsr_r const size_t estimated_replay_buffer_packets = calculate_estimated_replay_buffer_packets(self->settings.replay_buffer_size_secs, self->settings.fps, self->settings.audio_codec, self->audio_input_tracks); self->recording_clock = gsr_recording_clock_create(); - if(!self->recording_clock) - _exit(1); + if(!self->recording_clock) { + error_code = GSR_ERROR_GENERIC; + goto fail; + } + self->encoder_initialized = true; if(!gsr_encoder_init(&self->encoder, self->settings.replay_storage, estimated_replay_buffer_packets, self->settings.replay_buffer_size_secs, self->settings.filename)) { - gsr_log(GSR_LOG_LEVEL_ERROR, "failed to create self->encoder"); - _exit(1); + gsr_log(GSR_LOG_LEVEL_ERROR, "failed to create encoder"); + error_code = GSR_ERROR_GENERIC; + goto fail; } self->video_encoder = create_video_encoder(self->egl, &self->settings); if(!self->video_encoder) { - gsr_log(GSR_LOG_LEVEL_ERROR, "failed to create video self->encoder"); - _exit(1); + gsr_log(GSR_LOG_LEVEL_ERROR, "failed to create video encoder"); + error_code = GSR_ERROR_GENERIC; + goto fail; } if(!gsr_video_encoder_start(self->video_encoder, self->video_codec_context, self->video_frame)) { - gsr_log(GSR_LOG_LEVEL_ERROR, "failed to start video self->encoder"); - _exit(1); + gsr_log(GSR_LOG_LEVEL_ERROR, "failed to start video encoder"); + error_code = GSR_ERROR_GENERIC; + goto fail; } self->video_size.x = self->video_codec_context->width; @@ -241,8 +249,11 @@ gsr_recorder* gsr_recorder_create(const gsr_recorder_params *params, const gsr_r memset(&self->plugins, 0, sizeof(self->plugins)); - if(gsr_load_plugins(&self->plugins, self->plugin_filepaths, self->num_plugin_filepaths, &self->settings, self->egl, self->video_size) != GSR_ERROR_OK) - _exit(1); + if(gsr_load_plugins(&self->plugins, self->plugin_filepaths, self->num_plugin_filepaths, &self->settings, self->egl, self->video_size) != GSR_ERROR_OK) { + error_code = GSR_ERROR_GENERIC; + goto fail; + + } gsr_color_conversion_params color_conversion_params; memset(&color_conversion_params, 0, sizeof(color_conversion_params)); @@ -251,9 +262,11 @@ gsr_recorder* gsr_recorder_create(const gsr_recorder_params *params, const gsr_r color_conversion_params.load_external_image_shader = gsr_video_sources_uses_external_image(self->video_sources); gsr_video_encoder_get_textures(self->video_encoder, color_conversion_params.destination_textures, color_conversion_params.destination_textures_size, &color_conversion_params.num_destination_textures, &color_conversion_params.destination_color); + self->color_conversion_initialized = true; if(gsr_color_conversion_init(&self->color_conversion, &color_conversion_params) != 0) { gsr_log(GSR_LOG_LEVEL_ERROR, "main: failed to create color conversion"); - _exit(1); + error_code = GSR_ERROR_GENERIC; + goto fail; } gsr_color_conversion_clear(&self->color_conversion); @@ -261,25 +274,32 @@ gsr_recorder* gsr_recorder_create(const gsr_recorder_params *params, const gsr_r self->output_color_conversion = self->plugins.num_plugins > 0 ? &self->plugins.color_conversion : &self->color_conversion; if(self->settings.video_encoder == GSR_VIDEO_ENCODER_HW_CPU) { - if(!open_video_software(self->video_codec_context, &self->settings)) - _exit(1); + if(!open_video_software(self->video_codec_context, &self->settings)) { + error_code = GSR_ERROR_GENERIC; + goto fail; + } } else { - if(!open_video_hardware(self->video_codec_context, self->low_power, self->egl, &self->settings)) - _exit(1); + if(!open_video_hardware(self->video_codec_context, self->low_power, self->egl, &self->settings)) { + error_code = GSR_ERROR_GENERIC; + goto fail; + } } if(self->video_stream) { avcodec_parameters_from_context(self->video_stream->codecpar, self->video_codec_context); const size_t video_destination_id = gsr_encoder_add_recording_destination(&self->encoder, self->video_codec_context, self->av_format_context, self->video_stream, 0); if(self->settings.write_first_frame_ts && video_destination_id != (size_t)-1) { - char ts_filepath[PATH_MAX]; + char ts_filepath[PATH_MAX + 4]; snprintf(ts_filepath, sizeof(ts_filepath), "%s.ts", self->settings.filename); gsr_encoder_set_recording_destination_first_frame_ts_filepath(&self->encoder, video_destination_id, ts_filepath); } } - if(gsr_audio_capture_init(&self->audio_capture, &self->encoder, self->recording_clock, &self->running) != GSR_ERROR_OK) - _exit(1); + if(gsr_audio_capture_init(&self->audio_capture, &self->encoder, self->recording_clock, &self->running) != GSR_ERROR_OK) { + error_code = GSR_ERROR_GENERIC; + goto fail; + + } int audio_stream_index = GSR_VIDEO_STREAM_INDEX + 1; @@ -287,8 +307,10 @@ gsr_recorder* gsr_recorder_create(const gsr_recorder_params *params, const gsr_r const gsr_merged_audio_inputs *merged_audio_inputs = &self->audio_input_tracks->items[audio_track_index]; const bool use_amix = gsr_audio_inputs_should_use_amix(merged_audio_inputs); AVCodecContext *audio_codec_context = create_audio_codec_context(self->settings.fps, self->settings.audio_codec, use_amix, self->settings.audio_bitrate); - if(!audio_codec_context) - _exit(1); + if(!audio_codec_context) { + error_code = GSR_ERROR_GENERIC; + goto fail; + } AVStream *audio_stream = NULL; if(!self->settings.is_replaying) { @@ -300,8 +322,11 @@ gsr_recorder* gsr_recorder_create(const gsr_recorder_params *params, const gsr_r if(audio_stream && merged_audio_inputs->track_name[0] != '\0' && !self->settings.exclude_metadata) av_dict_set(&audio_stream->metadata, "title", merged_audio_inputs->track_name, 0); - if(!open_audio(audio_codec_context, self->settings.ffmpeg_audio_opts)) - _exit(1); + if(!open_audio(audio_codec_context, self->settings.ffmpeg_audio_opts)) { + error_code = GSR_ERROR_GENERIC; + goto fail; + + } if(audio_stream) avcodec_parameters_from_context(audio_stream->codecpar, audio_codec_context); @@ -319,12 +344,14 @@ gsr_recorder* gsr_recorder_create(const gsr_recorder_params *params, const gsr_r if(use_amix) { if(merged_audio_inputs->num_items > GSR_MAX_AUDIO_SOURCES_PER_TRACK) { gsr_log(GSR_LOG_LEVEL_ERROR, "too many audio sources for one audio track, the maximum is %d", GSR_MAX_AUDIO_SOURCES_PER_TRACK); - _exit(1); + error_code = GSR_ERROR_GENERIC; + goto fail; } if(gsr_audio_init_filter_graph(audio_codec_context, &graph, &sink, src_filter_ctx, merged_audio_inputs->num_items) < 0) { gsr_log(GSR_LOG_LEVEL_ERROR, "failed to create audio filter"); - _exit(1); + error_code = GSR_ERROR_GENERIC; + goto fail; } } @@ -355,11 +382,17 @@ gsr_recorder* gsr_recorder_create(const gsr_recorder_params *params, const gsr_r audio_track_result = gsr_audio_track_init_device_inputs(&audio_track, merged_audio_inputs, audio_codec_context, num_channels, num_audio_frames_shift, src_filter_ctx, use_amix); } - if(audio_track_result != GSR_ERROR_OK) - _exit(gsr_error_to_exit_code(audio_track_result)); + if(audio_track_result != GSR_ERROR_OK) { + error_code = audio_track_result; + goto fail; - if(!gsr_audio_capture_add_track(&self->audio_capture, &audio_track)) - _exit(1); + } + + if(!gsr_audio_capture_add_track(&self->audio_capture, &audio_track)) { + error_code = GSR_ERROR_GENERIC; + goto fail; + + } ++audio_stream_index; if(audio_codec_context->frame_size > self->audio_max_frame_size) @@ -372,7 +405,8 @@ gsr_recorder* gsr_recorder_create(const gsr_recorder_params *params, const gsr_r const int ret = avio_open(&self->av_format_context->pb, self->settings.filename, AVIO_FLAG_WRITE); if(ret < 0) { gsr_log(GSR_LOG_LEVEL_ERROR, "Could not open '%s': %s", self->settings.filename, gsr_av_error_to_string(ret)); - _exit(1); + error_code = GSR_ERROR_GENERIC; + goto fail; } } @@ -381,6 +415,11 @@ gsr_recorder* gsr_recorder_create(const gsr_recorder_params *params, const gsr_r *error = GSR_ERROR_OK; return self; + + fail: + *error = error_code; + gsr_recorder_destroy(self); + return NULL; } int gsr_recorder_run(gsr_recorder *self) { @@ -396,14 +435,13 @@ int gsr_recorder_run(gsr_recorder *self) { gsr_replay_save_init(&self->replay_save); - self->force_iframe_frame = false; gsr_recording_clock_start(self->recording_clock); const double record_start_time = gsr_recording_clock_get_start_time(self->recording_clock); if(gsr_audio_capture_start(&self->audio_capture, self->audio_max_frame_size, self->uses_amix) != GSR_ERROR_OK) - _exit(1); + return GSR_ERROR_GENERIC; // Set update_fps to 24 to test if duplicate/delayed frames cause video/audio desync or too fast/slow video. //const double update_fps = fps + 190; @@ -641,7 +679,7 @@ int gsr_recorder_run(gsr_recorder *self) { if(filepath_created && gsr_recording_output_start(&self->replay_recording_output, self->replay_recording_filepath, &self->settings, self->video_codec_context, &self->audio_capture, self->hdr, self->video_sources)) { const size_t video_recording_destination_id = gsr_encoder_add_recording_destination(&self->encoder, self->video_codec_context, self->replay_recording_output.av_format_context, self->replay_recording_output.video_stream, self->video_frame->pts); if(self->settings.write_first_frame_ts && video_recording_destination_id != (size_t)-1) { - char ts_filepath[PATH_MAX]; + char ts_filepath[PATH_MAX + 4]; snprintf(ts_filepath, sizeof(ts_filepath), "%s.ts", self->replay_recording_filepath); gsr_encoder_set_recording_destination_first_frame_ts_filepath(&self->encoder, video_recording_destination_id, ts_filepath); } @@ -772,39 +810,43 @@ static void gsr_recorder_stop_recording(gsr_recorder *self) { //fprintf(stderr, "Failed to write trailer\n"); } - if(!self->settings.is_replaying && !(self->av_format_context->oformat->flags & AVFMT_NOFILE)) { - avio_close(self->av_format_context->pb); - avformat_free_context(self->av_format_context); - } - - gsr_damage_deinit(&self->damage); - gsr_color_conversion_deinit(&self->color_conversion); - gsr_video_encoder_destroy(self->video_encoder, self->video_codec_context); - gsr_encoder_deinit(&self->encoder); - gsr_video_sources_deinit(self->video_sources); -#ifdef GSR_APP_AUDIO - gsr_pipewire_audio_deinit(self->pipewire_audio); -#endif - gsr_capture_deps_deinit(self->capture_deps); - if(!self->settings.is_replaying && self->callbacks.recording_stopped) self->callbacks.recording_stopped(self->settings.filename, self->callbacks.userdata); - - if(self->windowing->display) { - // TODO: This causes a crash, why? maybe some other library dlclose xlib and that also happened to unload this??? - //XCloseDisplay(dpy); - } - - //gsr_egl_unload(self->egl); - //gsr_window_destroy(&window); - - //av_frame_free(&self->video_frame); } void gsr_recorder_destroy(gsr_recorder *self) { if(!self) return; + gsr_audio_capture_deinit(&self->audio_capture); + gsr_plugins_deinit(&self->plugins); + + if(self->use_damage_tracking) + gsr_damage_deinit(&self->damage); + + if(self->color_conversion_initialized) + gsr_color_conversion_deinit(&self->color_conversion); + + if(self->video_encoder) + gsr_video_encoder_destroy(self->video_encoder, self->video_codec_context); + + if(self->encoder_initialized) + gsr_encoder_deinit(&self->encoder); + + gsr_video_sources_deinit(&self->video_sources_data); + + if(self->video_frame) + av_frame_free(&self->video_frame); + + if(self->video_codec_context) + avcodec_free_context(&self->video_codec_context); + + if(self->av_format_context) { + if(self->av_format_context->pb && !(self->av_format_context->oformat->flags & AVFMT_NOFILE)) + avio_close(self->av_format_context->pb); + avformat_free_context(self->av_format_context); + } + gsr_recording_clock_destroy(self->recording_clock); free(self); } -- cgit v1.2.3 From b9ab1878ca353f672593f6fa9d698567c4f41370 Mon Sep 17 00:00:00 2001 From: dec05eba Date: Sat, 1 Aug 2026 23:32:24 +0200 Subject: Split the recorder setup and record loop into named functions --- src/recorder/recorder.c | 766 +++++++++++++++++++++++++----------------------- 1 file changed, 407 insertions(+), 359 deletions(-) diff --git a/src/recorder/recorder.c b/src/recorder/recorder.c index 9eb819f..7ace350 100644 --- a/src/recorder/recorder.c +++ b/src/recorder/recorder.c @@ -76,6 +76,15 @@ struct gsr_recorder { char replay_recording_filepath[PATH_MAX]; bool replay_recording; + double fps_start_time; + int fps_counter; + int damage_fps_counter; + bool paused; + double record_start_time; + int64_t video_pts_counter; + int64_t video_prev_pts; + bool hdr_metadata_set; + volatile sig_atomic_t running; volatile sig_atomic_t toggle_pause; volatile sig_atomic_t toggle_replay_recording; @@ -94,34 +103,7 @@ struct gsr_recorder { static void gsr_recorder_stop_recording(gsr_recorder *self); -gsr_recorder* gsr_recorder_create(const gsr_recorder_params *params, const gsr_recorder_callbacks *callbacks, int *error) { - *error = GSR_ERROR_GENERIC; - - gsr_recorder *self = calloc(1, sizeof(gsr_recorder)); - if(!self) { - gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_recorder_create: failed to allocate recorder"); - return NULL; - } - - self->settings = *params->settings; - if(callbacks) - self->callbacks = *callbacks; - self->windowing = params->windowing; - self->egl = ¶ms->windowing->egl; - self->window = params->windowing->window; - self->capture_deps = params->capture_deps; - self->capture_sources = params->capture_sources; - self->audio_input_tracks = params->audio_input_tracks; - self->running = 1; - self->audio_max_frame_size = 1024; - int error_code = GSR_ERROR_GENERIC; - self->hdr = video_codec_is_hdr(params->settings->video_codec); - self->plugin_filepaths = params->plugin_filepaths; - self->num_plugin_filepaths = params->num_plugin_filepaths; -#ifdef GSR_APP_AUDIO - self->pipewire_audio = params->pipewire_audio; -#endif - +static int recorder_setup_container(gsr_recorder *self) { // The output format is automatically guessed by the file extension avformat_alloc_output_context2(&self->av_format_context, NULL, self->settings.container_format, self->settings.filename); if (!self->av_format_context) { @@ -130,11 +112,9 @@ gsr_recorder* gsr_recorder_create(const gsr_recorder_params *params, const gsr_r } else { gsr_log(GSR_LOG_LEVEL_ERROR, "Failed to deduce container format from file extension. Use the '-c' option to specify container format"); args_parser_print_usage(); - error_code = GSR_ERROR_GENERIC; - goto fail; + return GSR_ERROR_GENERIC; } - error_code = GSR_ERROR_GENERIC; - goto fail; + return GSR_ERROR_GENERIC; } set_format_context_options(self->av_format_context); @@ -157,11 +137,14 @@ gsr_recorder* gsr_recorder_create(const gsr_recorder_params *params, const gsr_r self->uses_amix = gsr_audio_input_tracks_should_use_amix(self->audio_input_tracks); self->settings.audio_codec = select_audio_codec_with_fallback(self->settings.audio_codec, self->file_extension, self->uses_amix); + return GSR_ERROR_OK; +} + +static int recorder_setup_video_sources(gsr_recorder *self) { self->video_size = (vec2i){0, 0}; const int video_sources_result = gsr_video_sources_create(&self->video_sources_data, &self->settings, self->egl, self->capture_deps, false, self->capture_sources, &self->video_size); if(video_sources_result != GSR_ERROR_OK) { - error_code = video_sources_result; - goto fail; + return video_sources_result; } self->video_sources = &self->video_sources_data; @@ -175,25 +158,26 @@ gsr_recorder* gsr_recorder_create(const gsr_recorder_params *params, const gsr_r gsr_audio_input silent_audio_input; memset(&silent_audio_input, 0, sizeof(silent_audio_input)); if(!gsr_merged_audio_inputs_add(&silent_audio_track, &silent_audio_input) || !gsr_audio_input_tracks_add(self->audio_input_tracks, &silent_audio_track)) { - error_code = GSR_ERROR_GENERIC; - goto fail; + return GSR_ERROR_GENERIC; } } self->video_stream = NULL; + return GSR_ERROR_OK; +} + +static int recorder_setup_video_encoder(gsr_recorder *self) { if(self->settings.video_encoder == GSR_VIDEO_ENCODER_HW_CPU && self->settings.video_codec != (gsr_video_codec)GSR_VIDEO_CODEC_AUTO && self->settings.video_codec != GSR_VIDEO_CODEC_H264) { gsr_log(GSR_LOG_LEVEL_ERROR, "-encoder cpu was specified but a codec other than h264 was specified. -encoder cpu supports only h264 at the moment"); - error_code = GSR_ERROR_GENERIC; - goto fail; + return GSR_ERROR_GENERIC; } self->low_power = false; const AVCodec *video_codec_f = NULL; const int select_video_codec_result = select_video_codec_with_fallback(self->video_size, &self->settings, self->file_extension, self->egl, &self->low_power, &video_codec_f); if(select_video_codec_result != GSR_ERROR_OK) { - error_code = select_video_codec_result; - goto fail; + return select_video_codec_result; } const enum AVPixelFormat video_pix_fmt = get_pixel_format(self->settings.video_codec, self->egl->gpu_info.vendor, self->settings.video_encoder == GSR_VIDEO_ENCODER_HW_CPU); @@ -204,8 +188,7 @@ gsr_recorder* gsr_recorder_create(const gsr_recorder_params *params, const gsr_r self->video_frame = av_frame_alloc(); if(!self->video_frame) { gsr_log(GSR_LOG_LEVEL_ERROR, "Failed to allocate video frame"); - error_code = GSR_ERROR_GENERIC; - goto fail; + return GSR_ERROR_GENERIC; } self->video_frame->format = self->video_codec_context->pix_fmt; self->video_frame->width = self->video_size.x; @@ -219,28 +202,24 @@ gsr_recorder* gsr_recorder_create(const gsr_recorder_params *params, const gsr_r const size_t estimated_replay_buffer_packets = calculate_estimated_replay_buffer_packets(self->settings.replay_buffer_size_secs, self->settings.fps, self->settings.audio_codec, self->audio_input_tracks); self->recording_clock = gsr_recording_clock_create(); if(!self->recording_clock) { - error_code = GSR_ERROR_GENERIC; - goto fail; + return GSR_ERROR_GENERIC; } self->encoder_initialized = true; if(!gsr_encoder_init(&self->encoder, self->settings.replay_storage, estimated_replay_buffer_packets, self->settings.replay_buffer_size_secs, self->settings.filename)) { gsr_log(GSR_LOG_LEVEL_ERROR, "failed to create encoder"); - error_code = GSR_ERROR_GENERIC; - goto fail; + return GSR_ERROR_GENERIC; } self->video_encoder = create_video_encoder(self->egl, &self->settings); if(!self->video_encoder) { gsr_log(GSR_LOG_LEVEL_ERROR, "failed to create video encoder"); - error_code = GSR_ERROR_GENERIC; - goto fail; + return GSR_ERROR_GENERIC; } if(!gsr_video_encoder_start(self->video_encoder, self->video_codec_context, self->video_frame)) { gsr_log(GSR_LOG_LEVEL_ERROR, "failed to start video encoder"); - error_code = GSR_ERROR_GENERIC; - goto fail; + return GSR_ERROR_GENERIC; } self->video_size.x = self->video_codec_context->width; @@ -250,8 +229,7 @@ gsr_recorder* gsr_recorder_create(const gsr_recorder_params *params, const gsr_r memset(&self->plugins, 0, sizeof(self->plugins)); if(gsr_load_plugins(&self->plugins, self->plugin_filepaths, self->num_plugin_filepaths, &self->settings, self->egl, self->video_size) != GSR_ERROR_OK) { - error_code = GSR_ERROR_GENERIC; - goto fail; + return GSR_ERROR_GENERIC; } @@ -265,8 +243,7 @@ gsr_recorder* gsr_recorder_create(const gsr_recorder_params *params, const gsr_r self->color_conversion_initialized = true; if(gsr_color_conversion_init(&self->color_conversion, &color_conversion_params) != 0) { gsr_log(GSR_LOG_LEVEL_ERROR, "main: failed to create color conversion"); - error_code = GSR_ERROR_GENERIC; - goto fail; + return GSR_ERROR_GENERIC; } gsr_color_conversion_clear(&self->color_conversion); @@ -275,13 +252,11 @@ gsr_recorder* gsr_recorder_create(const gsr_recorder_params *params, const gsr_r if(self->settings.video_encoder == GSR_VIDEO_ENCODER_HW_CPU) { if(!open_video_software(self->video_codec_context, &self->settings)) { - error_code = GSR_ERROR_GENERIC; - goto fail; + return GSR_ERROR_GENERIC; } } else { if(!open_video_hardware(self->video_codec_context, self->low_power, self->egl, &self->settings)) { - error_code = GSR_ERROR_GENERIC; - goto fail; + return GSR_ERROR_GENERIC; } } @@ -295,9 +270,12 @@ gsr_recorder* gsr_recorder_create(const gsr_recorder_params *params, const gsr_r } } + return GSR_ERROR_OK; +} + +static int recorder_setup_audio(gsr_recorder *self) { if(gsr_audio_capture_init(&self->audio_capture, &self->encoder, self->recording_clock, &self->running) != GSR_ERROR_OK) { - error_code = GSR_ERROR_GENERIC; - goto fail; + return GSR_ERROR_GENERIC; } @@ -308,8 +286,7 @@ gsr_recorder* gsr_recorder_create(const gsr_recorder_params *params, const gsr_r const bool use_amix = gsr_audio_inputs_should_use_amix(merged_audio_inputs); AVCodecContext *audio_codec_context = create_audio_codec_context(self->settings.fps, self->settings.audio_codec, use_amix, self->settings.audio_bitrate); if(!audio_codec_context) { - error_code = GSR_ERROR_GENERIC; - goto fail; + return GSR_ERROR_GENERIC; } AVStream *audio_stream = NULL; @@ -323,8 +300,7 @@ gsr_recorder* gsr_recorder_create(const gsr_recorder_params *params, const gsr_r av_dict_set(&audio_stream->metadata, "title", merged_audio_inputs->track_name, 0); if(!open_audio(audio_codec_context, self->settings.ffmpeg_audio_opts)) { - error_code = GSR_ERROR_GENERIC; - goto fail; + return GSR_ERROR_GENERIC; } if(audio_stream) @@ -344,14 +320,12 @@ gsr_recorder* gsr_recorder_create(const gsr_recorder_params *params, const gsr_r if(use_amix) { if(merged_audio_inputs->num_items > GSR_MAX_AUDIO_SOURCES_PER_TRACK) { gsr_log(GSR_LOG_LEVEL_ERROR, "too many audio sources for one audio track, the maximum is %d", GSR_MAX_AUDIO_SOURCES_PER_TRACK); - error_code = GSR_ERROR_GENERIC; - goto fail; + return GSR_ERROR_GENERIC; } if(gsr_audio_init_filter_graph(audio_codec_context, &graph, &sink, src_filter_ctx, merged_audio_inputs->num_items) < 0) { gsr_log(GSR_LOG_LEVEL_ERROR, "failed to create audio filter"); - error_code = GSR_ERROR_GENERIC; - goto fail; + return GSR_ERROR_GENERIC; } } @@ -383,14 +357,12 @@ gsr_recorder* gsr_recorder_create(const gsr_recorder_params *params, const gsr_r } if(audio_track_result != GSR_ERROR_OK) { - error_code = audio_track_result; - goto fail; + return audio_track_result; } if(!gsr_audio_capture_add_track(&self->audio_capture, &audio_track)) { - error_code = GSR_ERROR_GENERIC; - goto fail; + return GSR_ERROR_GENERIC; } ++audio_stream_index; @@ -401,18 +373,68 @@ gsr_recorder* gsr_recorder_create(const gsr_recorder_params *params, const gsr_r //av_dump_format(self->av_format_context, 0, filename, 1); + return GSR_ERROR_OK; +} + +static int recorder_open_output(gsr_recorder *self) { if(!self->settings.is_replaying && !(self->av_format_context->oformat->flags & AVFMT_NOFILE)) { const int ret = avio_open(&self->av_format_context->pb, self->settings.filename, AVIO_FLAG_WRITE); if(ret < 0) { gsr_log(GSR_LOG_LEVEL_ERROR, "Could not open '%s': %s", self->settings.filename, gsr_av_error_to_string(ret)); - error_code = GSR_ERROR_GENERIC; - goto fail; + return GSR_ERROR_GENERIC; } } if(!self->settings.is_replaying) av_write_header(self->av_format_context, self->settings.ffmpeg_opts); + return GSR_ERROR_OK; +} + +gsr_recorder* gsr_recorder_create(const gsr_recorder_params *params, const gsr_recorder_callbacks *callbacks, int *error) { + *error = GSR_ERROR_GENERIC; + + gsr_recorder *self = calloc(1, sizeof(gsr_recorder)); + if(!self) { + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_recorder_create: failed to allocate recorder"); + return NULL; + } + + self->settings = *params->settings; + if(callbacks) + self->callbacks = *callbacks; + self->windowing = params->windowing; + self->egl = ¶ms->windowing->egl; + self->window = params->windowing->window; + self->capture_deps = params->capture_deps; + self->capture_sources = params->capture_sources; + self->audio_input_tracks = params->audio_input_tracks; + self->running = 1; + self->audio_max_frame_size = 1024; + int error_code = GSR_ERROR_GENERIC; + self->hdr = video_codec_is_hdr(params->settings->video_codec); + self->plugin_filepaths = params->plugin_filepaths; + self->num_plugin_filepaths = params->num_plugin_filepaths; +#ifdef GSR_APP_AUDIO + self->pipewire_audio = params->pipewire_audio; +#endif + + const struct { + int (*setup)(gsr_recorder *self); + } setup_phases[] = { + { recorder_setup_container }, + { recorder_setup_video_sources }, + { recorder_setup_video_encoder }, + { recorder_setup_audio }, + { recorder_open_output }, + }; + + for(size_t i = 0; i < sizeof(setup_phases)/sizeof(setup_phases[0]); ++i) { + error_code = setup_phases[i].setup(self); + if(error_code != GSR_ERROR_OK) + goto fail; + } + *error = GSR_ERROR_OK; return self; @@ -421,354 +443,380 @@ gsr_recorder* gsr_recorder_create(const gsr_recorder_params *params, const gsr_r gsr_recorder_destroy(self); return NULL; } +static void recorder_process_events(gsr_recorder *self) { +while(gsr_window_process_event(self->window)) { + if(self->capture_deps->x11_cursor_display && self->settings.record_cursor) + gsr_cursor_on_event(&self->capture_deps->x11_cursor, gsr_window_get_event_data(self->window)); + + gsr_damage_on_event(&self->damage, gsr_window_get_event_data(self->window)); + for(size_t video_source_index = 0; video_source_index < self->video_sources->num_items; ++video_source_index) { +gsr_video_source *video_source = &self->video_sources->items[video_source_index]; + gsr_capture_on_event(video_source->capture, self->egl); + } +} -int gsr_recorder_run(gsr_recorder *self) { - double fps_start_time = clock_get_monotonic_seconds(); - //double frame_timer_start = fps_start_time; - int fps_counter = 0; - int damage_fps_counter = 0; - - bool paused = false; - self->replay_recording = false; - - memset(&self->replay_recording_output, 0, sizeof(self->replay_recording_output)); - - gsr_replay_save_init(&self->replay_save); - - self->force_iframe_frame = false; - - gsr_recording_clock_start(self->recording_clock); - const double record_start_time = gsr_recording_clock_get_start_time(self->recording_clock); - - if(gsr_audio_capture_start(&self->audio_capture, self->audio_max_frame_size, self->uses_amix) != GSR_ERROR_OK) - return GSR_ERROR_GENERIC; +if(self->capture_deps->x11_cursor_display && self->settings.record_cursor) + gsr_cursor_tick(&self->capture_deps->x11_cursor, DefaultRootWindow(self->capture_deps->x11_cursor_display)); +} - // Set update_fps to 24 to test if duplicate/delayed frames cause video/audio desync or too fast/slow video. - //const double update_fps = fps + 190; - self->should_stop_error = false; +static bool recorder_tick_video_sources(gsr_recorder *self) { +gsr_damage_tick(&self->damage); - int64_t video_pts_counter = 0; - int64_t video_prev_pts = 0; +self->should_stop_error = false; +bool damaged = false; - bool hdr_metadata_set = false; - self->hdr = video_codec_is_hdr(self->settings.video_codec); +if(self->use_damage_tracking) + damaged = gsr_damage_is_damaged(&self->damage); - - memset(&self->damage, 0, sizeof(self->damage)); - if(self->settings.framerate_mode == GSR_FRAMERATE_MODE_CONTENT && gsr_capture_sources_has_damage_tracked_target(self->capture_sources)) { - if(gsr_window_get_display_server(self->window) == GSR_DISPLAY_SERVER_X11) { - gsr_damage_init(&self->damage, self->egl, &self->capture_deps->x11_cursor, self->settings.record_cursor); - self->use_damage_tracking = true; +for(size_t video_source_index = 0; video_source_index < self->video_sources->num_items; ++video_source_index) { +gsr_video_source *video_source = &self->video_sources->items[video_source_index]; + gsr_capture_tick(video_source->capture); - for(size_t i = 0; i < self->capture_sources->num_items; ++i) { - const gsr_capture_source *capture_source = &self->capture_sources->items[i]; - switch(capture_source->type) { - case GSR_CAPTURE_SOURCE_TYPE_WINDOW: - gsr_damage_start_tracking_window(&self->damage, capture_source->window_id); - break; - case GSR_CAPTURE_SOURCE_TYPE_MONITOR: - case GSR_CAPTURE_SOURCE_TYPE_REGION: - // TODO: When capturing a region only track damage in that region - gsr_damage_start_tracking_monitor(&self->damage, capture_source->name); - break; - default: - break; - } - } - } else if(gsr_capture_sources_has_monitor_or_region(self->capture_sources)) { - gsr_log(GSR_LOG_LEVEL_WARNING, "\"-fm content\" has no effect on Wayland when recording a monitor. Either record a monitor on X11 or capture with desktop portal instead (-w portal)"); - } + if(gsr_capture_should_stop(video_source->capture, &self->should_stop_error)) { + self->running = 0; + break; } - while(self->running) { - while(gsr_window_process_event(self->window)) { - if(self->capture_deps->x11_cursor_display && self->settings.record_cursor) - gsr_cursor_on_event(&self->capture_deps->x11_cursor, gsr_window_get_event_data(self->window)); - - gsr_damage_on_event(&self->damage, gsr_window_get_event_data(self->window)); - for(size_t video_source_index = 0; video_source_index < self->video_sources->num_items; ++video_source_index) { - gsr_video_source *video_source = &self->video_sources->items[video_source_index]; - gsr_capture_on_event(video_source->capture, self->egl); - } - } + if(video_source->capture_source->type == GSR_CAPTURE_SOURCE_TYPE_FOCUSED_WINDOW) { + assert(video_source->capture->get_window_id); + const Window damage_target_window = video_source->capture->get_window_id(video_source->capture); - if(self->capture_deps->x11_cursor_display && self->settings.record_cursor) - gsr_cursor_tick(&self->capture_deps->x11_cursor, DefaultRootWindow(self->capture_deps->x11_cursor_display)); + if((int64_t)damage_target_window != video_source->capture_source->window_id) { + gsr_damage_stop_tracking_window(&self->damage, video_source->capture_source->window_id); + if(damage_target_window != 0) + gsr_damage_start_tracking_window(&self->damage, damage_target_window); + } - gsr_damage_tick(&self->damage); + video_source->capture_source->window_id = damage_target_window; + } - self->should_stop_error = false; - bool damaged = false; + if(video_source->capture->is_damaged) + damaged |= video_source->capture->is_damaged(video_source->capture); + else if(!self->use_damage_tracking) + damaged = true; +} - if(self->use_damage_tracking) - damaged = gsr_damage_is_damaged(&self->damage); +damaged |= gsr_plugins_is_damaged(&self->plugins); - for(size_t video_source_index = 0; video_source_index < self->video_sources->num_items; ++video_source_index) { - gsr_video_source *video_source = &self->video_sources->items[video_source_index]; - gsr_capture_tick(video_source->capture); +// TODO: Readd wayland sync warning when removing this +if(self->settings.framerate_mode != GSR_FRAMERATE_MODE_CONTENT) + damaged = true; - if(gsr_capture_should_stop(video_source->capture, &self->should_stop_error)) { - self->running = 0; - break; - } +if(damaged) + ++self->damage_fps_counter; - if(video_source->capture_source->type == GSR_CAPTURE_SOURCE_TYPE_FOCUSED_WINDOW) { - assert(video_source->capture->get_window_id); - const Window damage_target_window = video_source->capture->get_window_id(video_source->capture); + return damaged; +} - if((int64_t)damage_target_window != video_source->capture_source->window_id) { - gsr_damage_stop_tracking_window(&self->damage, video_source->capture_source->window_id); - if(damage_target_window != 0) - gsr_damage_start_tracking_window(&self->damage, damage_target_window); - } +static void recorder_update_fps_counters(gsr_recorder *self, bool damaged) { +++self->fps_counter; +const double time_now = clock_get_monotonic_seconds(); +//const double frame_timer_elapsed = time_now - frame_timer_start; +const double elapsed = time_now - self->fps_start_time; +if (elapsed >= 1.0) { + if(self->settings.verbose) { + fprintf(stderr, "update fps: %d, damage fps: %d\n", self->fps_counter, self->damage_fps_counter); + } + self->fps_start_time = time_now; + self->fps_counter = 0; + self->damage_fps_counter = 0; +} +} - video_source->capture_source->window_id = damage_target_window; - } +static void recorder_capture_and_encode_frame(gsr_recorder *self, bool damaged) { +const double this_video_frame_time = gsr_recording_clock_get_time(self->recording_clock); +const int64_t expected_frames = floor((this_video_frame_time - self->record_start_time) / self->target_fps); +const int64_t num_missed_frames = expected_frames - self->video_pts_counter; - if(video_source->capture->is_damaged) - damaged |= video_source->capture->is_damaged(video_source->capture); - else if(!self->use_damage_tracking) - damaged = true; - } +if(damaged && num_missed_frames >= 1 && !self->paused) { + // TODO: Dont do this if no damage? + self->egl->glClear(0); - damaged |= gsr_plugins_is_damaged(&self->plugins); + gsr_damage_clear(&self->damage); + gsr_plugins_clear_damage(&self->plugins); + gsr_capture_deps_cleanup_kms_fds(self->capture_deps); - // TODO: Readd wayland sync warning when removing this - if(self->settings.framerate_mode != GSR_FRAMERATE_MODE_CONTENT) - damaged = true; + gsr_capture_deps_update_kms(self->capture_deps); - if(damaged) - ++damage_fps_counter; + bool capture_has_synchronous_task = false; + for(size_t video_source_index = 0; video_source_index < self->video_sources->num_items; ++video_source_index) { +gsr_video_source *video_source = &self->video_sources->items[video_source_index]; + if(video_source->capture->clear_damage) + video_source->capture->clear_damage(video_source->capture); - ++fps_counter; - const double time_now = clock_get_monotonic_seconds(); - //const double frame_timer_elapsed = time_now - frame_timer_start; - const double elapsed = time_now - fps_start_time; - if (elapsed >= 1.0) { - if(self->settings.verbose) { - fprintf(stderr, "update fps: %d, damage fps: %d\n", fps_counter, damage_fps_counter); + if(video_source->capture->capture_has_synchronous_task) { + capture_has_synchronous_task = video_source->capture->capture_has_synchronous_task(video_source->capture); + if(capture_has_synchronous_task) { + self->paused = true; + gsr_recording_clock_set_paused(self->recording_clock, true); } - fps_start_time = time_now; - fps_counter = 0; - damage_fps_counter = 0; } + } - const double this_video_frame_time = gsr_recording_clock_get_time(self->recording_clock); - const int64_t expected_frames = floor((this_video_frame_time - record_start_time) / self->target_fps); - const int64_t num_missed_frames = expected_frames - video_pts_counter; + for(size_t video_source_index = 0; video_source_index < self->video_sources->num_items; ++video_source_index) { +gsr_video_source *video_source = &self->video_sources->items[video_source_index]; + if(video_source->capture->pre_capture) + video_source->capture->pre_capture(video_source->capture, &video_source->metadata, self->output_color_conversion); + } - if(damaged && num_missed_frames >= 1 && !paused) { - // TODO: Dont do this if no damage? - self->egl->glClear(0); + if(self->output_color_conversion->schedule_clear) { + self->output_color_conversion->schedule_clear = false; + gsr_color_conversion_clear(self->output_color_conversion); + } - gsr_damage_clear(&self->damage); - gsr_plugins_clear_damage(&self->plugins); - gsr_capture_deps_cleanup_kms_fds(self->capture_deps); + for(size_t video_source_index = 0; video_source_index < self->video_sources->num_items; ++video_source_index) { +gsr_video_source *video_source = &self->video_sources->items[video_source_index]; + gsr_capture_capture(video_source->capture, &video_source->metadata, self->output_color_conversion); + } - gsr_capture_deps_update_kms(self->capture_deps); + gsr_capture_deps_cleanup_kms_fds(self->capture_deps); - bool capture_has_synchronous_task = false; - for(size_t video_source_index = 0; video_source_index < self->video_sources->num_items; ++video_source_index) { - gsr_video_source *video_source = &self->video_sources->items[video_source_index]; - if(video_source->capture->clear_damage) - video_source->capture->clear_damage(video_source->capture); + if(self->plugins.num_plugins > 0) { + gsr_plugins_draw(&self->plugins); + gsr_color_conversion_draw(&self->color_conversion, self->plugins.texture, + (vec2i){0, 0}, self->video_size, + (vec2i){0, 0}, self->video_size, + self->video_size, GSR_ROT_0, GSR_FLIP_NONE, GSR_SOURCE_COLOR_RGB, false); + } - if(video_source->capture->capture_has_synchronous_task) { - capture_has_synchronous_task = video_source->capture->capture_has_synchronous_task(video_source->capture); - if(capture_has_synchronous_task) { - paused = true; - gsr_recording_clock_set_paused(self->recording_clock, true); - } - } - } + if(capture_has_synchronous_task) { + self->paused = false; + gsr_recording_clock_set_paused(self->recording_clock, false); + } - for(size_t video_source_index = 0; video_source_index < self->video_sources->num_items; ++video_source_index) { - gsr_video_source *video_source = &self->video_sources->items[video_source_index]; - if(video_source->capture->pre_capture) - video_source->capture->pre_capture(video_source->capture, &video_source->metadata, self->output_color_conversion); - } + gsr_egl_swap_buffers(self->egl); + gsr_video_encoder_copy_textures_to_frame(self->video_encoder, self->video_frame, self->output_color_conversion); - if(self->output_color_conversion->schedule_clear) { - self->output_color_conversion->schedule_clear = false; - gsr_color_conversion_clear(self->output_color_conversion); - } + for(size_t video_source_index = 0; video_source_index < self->video_sources->num_items; ++video_source_index) { +gsr_video_source *video_source = &self->video_sources->items[video_source_index]; + if(self->hdr && !self->hdr_metadata_set && !self->settings.is_replaying && add_hdr_metadata_to_video_stream(video_source->capture, self->video_stream)) + self->hdr_metadata_set = true; + } - for(size_t video_source_index = 0; video_source_index < self->video_sources->num_items; ++video_source_index) { - gsr_video_source *video_source = &self->video_sources->items[video_source_index]; - gsr_capture_capture(video_source->capture, &video_source->metadata, self->output_color_conversion); - } + // TODO: Check if duplicate frame can be saved just by writing it with a different pts instead of sending it again + const int num_frames_to_encode = self->settings.framerate_mode == GSR_FRAMERATE_MODE_CONSTANT ? num_missed_frames : 1; + for(int i = 0; i < num_frames_to_encode; ++i) { + if(self->settings.framerate_mode == GSR_FRAMERATE_MODE_CONSTANT) { + self->video_frame->pts = self->video_pts_counter + i; + } else { + self->video_frame->pts = (this_video_frame_time - self->record_start_time) * (double)AV_TIME_BASE; + const bool same_pts = self->video_frame->pts == self->video_prev_pts; + self->video_prev_pts = self->video_frame->pts; + if(same_pts) + continue; + } - gsr_capture_deps_cleanup_kms_fds(self->capture_deps); + if(self->force_iframe_frame) { + self->video_frame->pict_type = AV_PICTURE_TYPE_I; + } - if(self->plugins.num_plugins > 0) { - gsr_plugins_draw(&self->plugins); - gsr_color_conversion_draw(&self->color_conversion, self->plugins.texture, - (vec2i){0, 0}, self->video_size, - (vec2i){0, 0}, self->video_size, - self->video_size, GSR_ROT_0, GSR_FLIP_NONE, GSR_SOURCE_COLOR_RGB, false); - } + int ret = avcodec_send_frame(self->video_codec_context, self->video_frame); + if(ret == 0) { + // TODO: Move to separate thread because this could write to network (for example when livestreaming) + gsr_encoder_receive_packets(&self->encoder, self->video_codec_context, self->video_frame->pts, GSR_VIDEO_STREAM_INDEX); + } else { + gsr_log(GSR_LOG_LEVEL_ERROR, "avcodec_send_frame failed, error: %s", gsr_av_error_to_string(ret)); + } - if(capture_has_synchronous_task) { - paused = false; - gsr_recording_clock_set_paused(self->recording_clock, false); - } + if(self->force_iframe_frame) { + self->force_iframe_frame = false; + self->video_frame->pict_type = AV_PICTURE_TYPE_NONE; + } + } - gsr_egl_swap_buffers(self->egl); - gsr_video_encoder_copy_textures_to_frame(self->video_encoder, self->video_frame, self->output_color_conversion); + self->video_pts_counter += num_missed_frames; +} +} - for(size_t video_source_index = 0; video_source_index < self->video_sources->num_items; ++video_source_index) { - gsr_video_source *video_source = &self->video_sources->items[video_source_index]; - if(self->hdr && !hdr_metadata_set && !self->settings.is_replaying && add_hdr_metadata_to_video_stream(video_source->capture, self->video_stream)) - hdr_metadata_set = true; - } +static void recorder_apply_pause_toggle(gsr_recorder *self) { + if(self->toggle_pause == 1 && !self->settings.is_replaying) { + self->paused = !self->paused; + gsr_recording_clock_set_paused(self->recording_clock, self->paused); + gsr_log(GSR_LOG_LEVEL_INFO, self->paused ? "Paused" : "Unpaused"); + self->toggle_pause = 0; + } +} - // TODO: Check if duplicate frame can be saved just by writing it with a different pts instead of sending it again - const int num_frames_to_encode = self->settings.framerate_mode == GSR_FRAMERATE_MODE_CONSTANT ? num_missed_frames : 1; - for(int i = 0; i < num_frames_to_encode; ++i) { - if(self->settings.framerate_mode == GSR_FRAMERATE_MODE_CONSTANT) { - self->video_frame->pts = video_pts_counter + i; - } else { - self->video_frame->pts = (this_video_frame_time - record_start_time) * (double)AV_TIME_BASE; - const bool same_pts = self->video_frame->pts == video_prev_pts; - video_prev_pts = self->video_frame->pts; - if(same_pts) - continue; - } +static void recorder_apply_replay_recording_toggle(gsr_recorder *self) { + if(self->toggle_replay_recording && !self->settings.replay_recording_directory) { + self->toggle_replay_recording = 0; + if(self->callbacks.recording_started) + self->callbacks.recording_started(NULL, self->callbacks.userdata); + } - if(self->force_iframe_frame) { - self->video_frame->pict_type = AV_PICTURE_TYPE_I; + if(self->toggle_replay_recording && self->settings.replay_recording_directory) { + self->toggle_replay_recording = 0; + const bool new_replay_recording_state = !self->replay_recording; + if(new_replay_recording_state) { + gsr_audio_capture_lock_filter(&self->audio_capture); + self->num_replay_recording_items = 0; + const bool filepath_created = gsr_create_new_recording_filepath_from_timestamp(self->replay_recording_filepath, sizeof(self->replay_recording_filepath), self->settings.replay_recording_directory, "Video", self->file_extension, self->settings.date_folders); + if(filepath_created && gsr_recording_output_start(&self->replay_recording_output, self->replay_recording_filepath, &self->settings, self->video_codec_context, &self->audio_capture, self->hdr, self->video_sources)) { + const size_t video_recording_destination_id = gsr_encoder_add_recording_destination(&self->encoder, self->video_codec_context, self->replay_recording_output.av_format_context, self->replay_recording_output.video_stream, self->video_frame->pts); + if(self->settings.write_first_frame_ts && video_recording_destination_id != (size_t)-1) { + char ts_filepath[PATH_MAX + 4]; + snprintf(ts_filepath, sizeof(ts_filepath), "%s.ts", self->replay_recording_filepath); + gsr_encoder_set_recording_destination_first_frame_ts_filepath(&self->encoder, video_recording_destination_id, ts_filepath); } - int ret = avcodec_send_frame(self->video_codec_context, self->video_frame); - if(ret == 0) { - // TODO: Move to separate thread because this could write to network (for example when livestreaming) - gsr_encoder_receive_packets(&self->encoder, self->video_codec_context, self->video_frame->pts, GSR_VIDEO_STREAM_INDEX); - } else { - gsr_log(GSR_LOG_LEVEL_ERROR, "avcodec_send_frame failed, error: %s", gsr_av_error_to_string(ret)); + if(video_recording_destination_id != (size_t)-1 && self->num_replay_recording_items < GSR_MAX_RECORDING_DESTINATIONS) { + self->replay_recording_items[self->num_replay_recording_items] = video_recording_destination_id; + ++self->num_replay_recording_items; } - if(self->force_iframe_frame) { - self->force_iframe_frame = false; - self->video_frame->pict_type = AV_PICTURE_TYPE_NONE; + for(size_t i = 0; i < self->replay_recording_output.num_audio_streams; ++i) { + const gsr_recording_audio_stream *audio_stream = &self->replay_recording_output.audio_streams[i]; + const size_t audio_recording_destination_id = gsr_encoder_add_recording_destination(&self->encoder, audio_stream->audio_track->codec_context, self->replay_recording_output.av_format_context, audio_stream->stream, audio_stream->audio_track->pts); + if(audio_recording_destination_id != (size_t)-1 && self->num_replay_recording_items < GSR_MAX_RECORDING_DESTINATIONS) { + self->replay_recording_items[self->num_replay_recording_items] = audio_recording_destination_id; + ++self->num_replay_recording_items; + } } + + self->replay_recording = true; + self->force_iframe_frame = true; + gsr_log(GSR_LOG_LEVEL_INFO, "Started recording"); + if(self->callbacks.recording_started) + self->callbacks.recording_started(self->replay_recording_filepath, self->callbacks.userdata); + } else { + if(self->callbacks.recording_started) + self->callbacks.recording_started(NULL, self->callbacks.userdata); + } + gsr_audio_capture_unlock_filter(&self->audio_capture); + } else if(self->replay_recording_output.av_format_context) { + for(size_t i = 0; i < self->num_replay_recording_items; ++i) { + gsr_encoder_remove_recording_destination(&self->encoder, self->replay_recording_items[i]); + } + self->num_replay_recording_items = 0; + + if(gsr_recording_output_stop(&self->replay_recording_output)) { + gsr_log(GSR_LOG_LEVEL_INFO, "Stopped recording"); + if(self->callbacks.recording_stopped) + self->callbacks.recording_stopped(self->replay_recording_filepath, self->callbacks.userdata); + } else { + if(self->callbacks.recording_stopped) + self->callbacks.recording_stopped(NULL, self->callbacks.userdata); } - video_pts_counter += num_missed_frames; + self->replay_recording = false; + self->replay_recording_filepath[0] = '\0'; } + } +} - if(self->toggle_pause == 1 && !self->settings.is_replaying) { - paused = !paused; - gsr_recording_clock_set_paused(self->recording_clock, paused); - gsr_log(GSR_LOG_LEVEL_INFO, paused ? "Paused" : "Unpaused"); - self->toggle_pause = 0; - } +static void recorder_poll_replay_save(gsr_recorder *self) { + bool replay_save_result = false; + const char *replay_save_output_filepath = NULL; + if(gsr_replay_save_poll(&self->replay_save, &replay_save_result, &replay_save_output_filepath)) { + if(self->callbacks.replay_saved) + self->callbacks.replay_saved(replay_save_output_filepath[0] == '\0' || !replay_save_result ? NULL : replay_save_output_filepath, self->callbacks.userdata); + } - if(self->toggle_replay_recording && !self->settings.replay_recording_directory) { - self->toggle_replay_recording = 0; - if(self->callbacks.recording_started) - self->callbacks.recording_started(NULL, self->callbacks.userdata); + if(self->save_replay_seconds != 0 && !gsr_replay_save_is_running(&self->replay_save) && self->settings.is_replaying) { + int current_save_replay_seconds = self->save_replay_seconds; + if(current_save_replay_seconds > 0) + current_save_replay_seconds += self->settings.keyint; + + self->save_replay_seconds = 0; + const bool replay_start_result = gsr_replay_save_start(&self->replay_save, self->video_codec_context, GSR_VIDEO_STREAM_INDEX, &self->audio_capture, &self->encoder, &self->settings, self->file_extension, self->hdr, self->video_sources, current_save_replay_seconds); + if(!replay_start_result && self->callbacks.replay_saved) + self->callbacks.replay_saved(NULL, self->callbacks.userdata); + + if(self->settings.restart_replay_on_save && current_save_replay_seconds == GSR_SAVE_REPLAY_SECONDS_FULL) { + pthread_mutex_lock(&self->encoder.replay_mutex); + gsr_replay_buffer_clear(self->encoder.replay_buffer); + pthread_mutex_unlock(&self->encoder.replay_mutex); } + } +} - if(self->toggle_replay_recording && self->settings.replay_recording_directory) { - self->toggle_replay_recording = 0; - const bool new_replay_recording_state = !self->replay_recording; - if(new_replay_recording_state) { - gsr_audio_capture_lock_filter(&self->audio_capture); - self->num_replay_recording_items = 0; - const bool filepath_created = gsr_create_new_recording_filepath_from_timestamp(self->replay_recording_filepath, sizeof(self->replay_recording_filepath), self->settings.replay_recording_directory, "Video", self->file_extension, self->settings.date_folders); - if(filepath_created && gsr_recording_output_start(&self->replay_recording_output, self->replay_recording_filepath, &self->settings, self->video_codec_context, &self->audio_capture, self->hdr, self->video_sources)) { - const size_t video_recording_destination_id = gsr_encoder_add_recording_destination(&self->encoder, self->video_codec_context, self->replay_recording_output.av_format_context, self->replay_recording_output.video_stream, self->video_frame->pts); - if(self->settings.write_first_frame_ts && video_recording_destination_id != (size_t)-1) { - char ts_filepath[PATH_MAX + 4]; - snprintf(ts_filepath, sizeof(ts_filepath), "%s.ts", self->replay_recording_filepath); - gsr_encoder_set_recording_destination_first_frame_ts_filepath(&self->encoder, video_recording_destination_id, ts_filepath); - } +static void recorder_sleep_until_next_frame(gsr_recorder *self) { + const double time_at_frame_end = gsr_recording_clock_get_time(self->recording_clock); + const double time_elapsed_total = time_at_frame_end - self->record_start_time; + const int64_t frames_elapsed = floor(time_elapsed_total / self->target_fps); + const double time_at_next_frame = (frames_elapsed + 1) * self->target_fps; + double time_to_next_frame = time_at_next_frame - time_elapsed_total; + if(time_to_next_frame > self->target_fps) + time_to_next_frame = self->target_fps; + const int64_t end_num_missed_frames = frames_elapsed - self->video_pts_counter; + + if(time_to_next_frame > 0.0 && end_num_missed_frames <= 0) + av_usleep(time_to_next_frame * 1000.0 * 1000.0); + else { + if(self->paused) + av_usleep(20.0 * 1000.0); // 20 milliseconds + else if(self->settings.framerate_mode == GSR_FRAMERATE_MODE_CONTENT) + av_usleep(2.8 * 1000.0); // 2.8 milliseconds + } +} - if(video_recording_destination_id != (size_t)-1 && self->num_replay_recording_items < GSR_MAX_RECORDING_DESTINATIONS) { - self->replay_recording_items[self->num_replay_recording_items] = video_recording_destination_id; - ++self->num_replay_recording_items; - } +int gsr_recorder_run(gsr_recorder *self) { + self->fps_start_time = clock_get_monotonic_seconds(); + //double frame_timer_start = self->fps_start_time; + self->fps_counter = 0; + self->damage_fps_counter = 0; - for(size_t i = 0; i < self->replay_recording_output.num_audio_streams; ++i) { - const gsr_recording_audio_stream *audio_stream = &self->replay_recording_output.audio_streams[i]; - const size_t audio_recording_destination_id = gsr_encoder_add_recording_destination(&self->encoder, audio_stream->audio_track->codec_context, self->replay_recording_output.av_format_context, audio_stream->stream, audio_stream->audio_track->pts); - if(audio_recording_destination_id != (size_t)-1 && self->num_replay_recording_items < GSR_MAX_RECORDING_DESTINATIONS) { - self->replay_recording_items[self->num_replay_recording_items] = audio_recording_destination_id; - ++self->num_replay_recording_items; - } - } + self->paused = false; + self->replay_recording = false; - self->replay_recording = true; - self->force_iframe_frame = true; - gsr_log(GSR_LOG_LEVEL_INFO, "Started recording"); - if(self->callbacks.recording_started) - self->callbacks.recording_started(self->replay_recording_filepath, self->callbacks.userdata); - } else { - if(self->callbacks.recording_started) - self->callbacks.recording_started(NULL, self->callbacks.userdata); - } - gsr_audio_capture_unlock_filter(&self->audio_capture); - } else if(self->replay_recording_output.av_format_context) { - for(size_t i = 0; i < self->num_replay_recording_items; ++i) { - gsr_encoder_remove_recording_destination(&self->encoder, self->replay_recording_items[i]); - } - self->num_replay_recording_items = 0; - - if(gsr_recording_output_stop(&self->replay_recording_output)) { - gsr_log(GSR_LOG_LEVEL_INFO, "Stopped recording"); - if(self->callbacks.recording_stopped) - self->callbacks.recording_stopped(self->replay_recording_filepath, self->callbacks.userdata); - } else { - if(self->callbacks.recording_stopped) - self->callbacks.recording_stopped(NULL, self->callbacks.userdata); - } + memset(&self->replay_recording_output, 0, sizeof(self->replay_recording_output)); - self->replay_recording = false; - self->replay_recording_filepath[0] = '\0'; - } - } + gsr_replay_save_init(&self->replay_save); - bool replay_save_result = false; - const char *replay_save_output_filepath = NULL; - if(gsr_replay_save_poll(&self->replay_save, &replay_save_result, &replay_save_output_filepath)) { - if(self->callbacks.replay_saved) - self->callbacks.replay_saved(replay_save_output_filepath[0] == '\0' || !replay_save_result ? NULL : replay_save_output_filepath, self->callbacks.userdata); - } + self->force_iframe_frame = false; - if(self->save_replay_seconds != 0 && !gsr_replay_save_is_running(&self->replay_save) && self->settings.is_replaying) { - int current_save_replay_seconds = self->save_replay_seconds; - if(current_save_replay_seconds > 0) - current_save_replay_seconds += self->settings.keyint; + gsr_recording_clock_start(self->recording_clock); + self->record_start_time = gsr_recording_clock_get_start_time(self->recording_clock); - self->save_replay_seconds = 0; - const bool replay_start_result = gsr_replay_save_start(&self->replay_save, self->video_codec_context, GSR_VIDEO_STREAM_INDEX, &self->audio_capture, &self->encoder, &self->settings, self->file_extension, self->hdr, self->video_sources, current_save_replay_seconds); - if(!replay_start_result && self->callbacks.replay_saved) - self->callbacks.replay_saved(NULL, self->callbacks.userdata); + if(gsr_audio_capture_start(&self->audio_capture, self->audio_max_frame_size, self->uses_amix) != GSR_ERROR_OK) + return GSR_ERROR_GENERIC; - if(self->settings.restart_replay_on_save && current_save_replay_seconds == GSR_SAVE_REPLAY_SECONDS_FULL) { - pthread_mutex_lock(&self->encoder.replay_mutex); - gsr_replay_buffer_clear(self->encoder.replay_buffer); - pthread_mutex_unlock(&self->encoder.replay_mutex); + // Set update_fps to 24 to test if duplicate/delayed frames cause video/audio desync or too fast/slow video. + //const double update_fps = fps + 190; + self->should_stop_error = false; + + self->video_pts_counter = 0; + self->video_prev_pts = 0; + + self->hdr_metadata_set = false; + self->hdr = video_codec_is_hdr(self->settings.video_codec); + + + memset(&self->damage, 0, sizeof(self->damage)); + if(self->settings.framerate_mode == GSR_FRAMERATE_MODE_CONTENT && gsr_capture_sources_has_damage_tracked_target(self->capture_sources)) { + if(gsr_window_get_display_server(self->window) == GSR_DISPLAY_SERVER_X11) { + gsr_damage_init(&self->damage, self->egl, &self->capture_deps->x11_cursor, self->settings.record_cursor); + self->use_damage_tracking = true; + + for(size_t i = 0; i < self->capture_sources->num_items; ++i) { + const gsr_capture_source *capture_source = &self->capture_sources->items[i]; + switch(capture_source->type) { + case GSR_CAPTURE_SOURCE_TYPE_WINDOW: + gsr_damage_start_tracking_window(&self->damage, capture_source->window_id); + break; + case GSR_CAPTURE_SOURCE_TYPE_MONITOR: + case GSR_CAPTURE_SOURCE_TYPE_REGION: + // TODO: When capturing a region only track damage in that region + gsr_damage_start_tracking_monitor(&self->damage, capture_source->name); + break; + default: + break; + } } + } else if(gsr_capture_sources_has_monitor_or_region(self->capture_sources)) { + gsr_log(GSR_LOG_LEVEL_WARNING, "\"-fm content\" has no effect on Wayland when recording a monitor. Either record a monitor on X11 or capture with desktop portal instead (-w portal)"); } + } - const double time_at_frame_end = gsr_recording_clock_get_time(self->recording_clock); - const double time_elapsed_total = time_at_frame_end - record_start_time; - const int64_t frames_elapsed = floor(time_elapsed_total / self->target_fps); - const double time_at_next_frame = (frames_elapsed + 1) * self->target_fps; - double time_to_next_frame = time_at_next_frame - time_elapsed_total; - if(time_to_next_frame > self->target_fps) - time_to_next_frame = self->target_fps; - const int64_t end_num_missed_frames = frames_elapsed - video_pts_counter; - - if(time_to_next_frame > 0.0 && end_num_missed_frames <= 0) - av_usleep(time_to_next_frame * 1000.0 * 1000.0); - else { - if(paused) - av_usleep(20.0 * 1000.0); // 20 milliseconds - else if(self->settings.framerate_mode == GSR_FRAMERATE_MODE_CONTENT) - av_usleep(2.8 * 1000.0); // 2.8 milliseconds - } + while(self->running) { + recorder_process_events(self); + const bool damaged = recorder_tick_video_sources(self); + recorder_update_fps_counters(self, damaged); + recorder_capture_and_encode_frame(self, damaged); + recorder_apply_pause_toggle(self); + recorder_apply_replay_recording_toggle(self); + recorder_poll_replay_save(self); + recorder_sleep_until_next_frame(self); } gsr_recorder_stop_recording(self); -- cgit v1.2.3 From 8f97b0b422820fd2ac0162aa12cdbf14150f1a11 Mon Sep 17 00:00:00 2001 From: dec05eba Date: Sat, 1 Aug 2026 23:41:07 +0200 Subject: Remove unused includes Most were left over from moving code out of main.cpp and from replacing fprintf with gsr_log. Also fixes building without the app_audio option. --- kms/client/kms_client.c | 2 +- src/capture/kms.c | 2 -- src/capture/nvfbc.c | 1 - src/capture/xcomposite.c | 4 ---- src/capture/ximage.c | 3 --- src/cli/commands.c | 1 - src/cli/main.c | 20 +++++++++----------- src/codec_query/nvenc.c | 1 - src/codec_query/vaapi.c | 1 - src/cuda.c | 2 -- src/cursor.c | 1 - src/damage.c | 1 - src/egl.c | 1 - src/image_writer.c | 1 - src/kde_night_light.c | 2 -- src/library_loader.c | 1 - src/plugins.c | 1 - src/recorder/audio_codec.c | 1 - src/recorder/codec_select.c | 1 - src/recorder/recording_clock.c | 1 - src/recorder/video_codec.c | 1 - src/replay_buffer/replay_buffer.c | 2 -- src/replay_buffer/replay_buffer_disk.c | 1 - src/replay_buffer/replay_buffer_ram.c | 1 - src/shader.c | 1 - src/sound.c | 1 - src/window/x11.c | 1 - 27 files changed, 10 insertions(+), 46 deletions(-) diff --git a/kms/client/kms_client.c b/kms/client/kms_client.c index 4d70d78..c0279d2 100644 --- a/kms/client/kms_client.c +++ b/kms/client/kms_client.c @@ -10,10 +10,10 @@ #include #include #include +#include #include #include #include -#include #ifdef __linux__ #include #endif diff --git a/src/capture/kms.c b/src/capture/kms.c index 4048a7f..2285fb6 100644 --- a/src/capture/kms.c +++ b/src/capture/kms.c @@ -2,13 +2,11 @@ #include "../../include/log.h" #include "../../include/utils.h" #include "../../include/color_conversion.h" -#include "../../include/cursor.h" #include "../../include/kde_night_light.h" #include "../../include/window/window.h" #include #include -#include #include #include diff --git a/src/capture/nvfbc.c b/src/capture/nvfbc.c index 41e2466..8f386d4 100644 --- a/src/capture/nvfbc.c +++ b/src/capture/nvfbc.c @@ -9,7 +9,6 @@ #include #include #include -#include #include #include diff --git a/src/capture/xcomposite.c b/src/capture/xcomposite.c index af48698..ace04b9 100644 --- a/src/capture/xcomposite.c +++ b/src/capture/xcomposite.c @@ -2,14 +2,10 @@ #include "../../include/log.h" #include "../../include/window_texture.h" #include "../../include/utils.h" -#include "../../include/cursor.h" #include "../../include/color_conversion.h" #include "../../include/window/window.h" #include -#include -#include -#include #include diff --git a/src/capture/ximage.c b/src/capture/ximage.c index becca82..d80cd0b 100644 --- a/src/capture/ximage.c +++ b/src/capture/ximage.c @@ -1,14 +1,11 @@ #include "../../include/capture/ximage.h" #include "../../include/log.h" #include "../../include/utils.h" -#include "../../include/cursor.h" #include "../../include/color_conversion.h" #include "../../include/window/window.h" #include -#include #include -#include #include diff --git a/src/cli/commands.c b/src/cli/commands.c index 4eaaf65..38342ec 100644 --- a/src/cli/commands.c +++ b/src/cli/commands.c @@ -15,7 +15,6 @@ #endif #include -#include #include #include #include diff --git a/src/cli/main.c b/src/cli/main.c index b74eb1c..563ab19 100644 --- a/src/cli/main.c +++ b/src/cli/main.c @@ -305,24 +305,26 @@ static void recording_stopped_callback(const char *filepath, void *userdata) { } #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_pipewire_audio *pipewire_audio, gsr_app_audio_names *app_audio_names) { +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)) { + 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); + gsr_pipewire_audio_for_each_app(&pipewire_audio, app_audio_name_callback, app_audio_names); return GSR_ERROR_OK; } #endif @@ -364,7 +366,7 @@ static int take_screenshot(args_parser *arg_parser, gsr_windowing *windowing, gs 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_pipewire_audio *pipewire_audio) { +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) { const Arg *plugin_arg = args_parser_get_arg(arg_parser, "-p"); assert(plugin_arg); @@ -378,9 +380,7 @@ static int record(args_parser *arg_parser, gsr_windowing *windowing, gsr_capture 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; -#else - (void)pipewire_audio; + recorder_params.pipewire_audio = &pipewire_audio; #endif gsr_recorder_callbacks callbacks; @@ -473,11 +473,9 @@ int main(int argc, char **argv) { gsr_app_audio_names app_audio_names; memset(&app_audio_names, 0, sizeof(app_audio_names)); - gsr_pipewire_audio pipewire_audio; - memset(&pipewire_audio, 0, sizeof(pipewire_audio)); #ifdef GSR_APP_AUDIO if(uses_app_audio) { - const int app_audio_result = setup_app_audio(&pipewire_audio, &app_audio_names); + const int app_audio_result = setup_app_audio(&app_audio_names); if(app_audio_result != GSR_ERROR_OK) _exit(gsr_error_to_exit_code(app_audio_result)); } @@ -543,7 +541,7 @@ int main(int argc, char **argv) { result = take_screenshot(&arg_parser, &windowing, &capture_deps, &capture_sources, image_format); } else { - result = record(&arg_parser, &windowing, &capture_deps, &capture_sources, &audio_input_tracks, &pipewire_audio); + result = record(&arg_parser, &windowing, &capture_deps, &capture_sources, &audio_input_tracks); } gsr_capture_deps_deinit(&capture_deps); diff --git a/src/codec_query/nvenc.c b/src/codec_query/nvenc.c index 3f8b442..4416b91 100644 --- a/src/codec_query/nvenc.c +++ b/src/codec_query/nvenc.c @@ -4,7 +4,6 @@ #include "../../external/nvEncodeAPI.h" #include -#include #include #define NVENCAPI_MAJOR_VERSION_470 11 diff --git a/src/codec_query/vaapi.c b/src/codec_query/vaapi.c index 4cc1bab..ca4b3f4 100644 --- a/src/codec_query/vaapi.c +++ b/src/codec_query/vaapi.c @@ -3,7 +3,6 @@ #include "../../include/utils.h" #include -#include #include #include #include diff --git a/src/cuda.c b/src/cuda.c index 55d1f40..54dac63 100644 --- a/src/cuda.c +++ b/src/cuda.c @@ -2,9 +2,7 @@ #include "../include/log.h" #include "../include/library_loader.h" #include -#include #include -#include bool gsr_cuda_load(gsr_cuda *self) { memset(self, 0, sizeof(gsr_cuda)); diff --git a/src/cursor.c b/src/cursor.c index 65d9d4c..e8595aa 100644 --- a/src/cursor.c +++ b/src/cursor.c @@ -1,7 +1,6 @@ #include "../include/cursor.h" #include "../include/log.h" -#include #include #include #include diff --git a/src/damage.c b/src/damage.c index 351f09a..cc122cc 100644 --- a/src/damage.c +++ b/src/damage.c @@ -3,7 +3,6 @@ #include "../include/utils.h" #include "../include/window/window.h" -#include #include #include #include diff --git a/src/egl.c b/src/egl.c index 7e29bbc..933a997 100644 --- a/src/egl.c +++ b/src/egl.c @@ -9,7 +9,6 @@ #include #include #include -#include // TODO: rename gsr_egl to something else since this includes both egl and glx and in the future maybe vulkan too diff --git a/src/image_writer.c b/src/image_writer.c index 5f3fe9e..117c8b8 100644 --- a/src/image_writer.c +++ b/src/image_writer.c @@ -9,7 +9,6 @@ #include #include #include -#include #include #define TJPF_RGBA 7 diff --git a/src/kde_night_light.c b/src/kde_night_light.c index 587e0c8..d3491ef 100644 --- a/src/kde_night_light.c +++ b/src/kde_night_light.c @@ -3,7 +3,6 @@ #ifdef GSR_DBUS -#include #include #include #include @@ -343,7 +342,6 @@ bool gsr_kde_night_light_get_inverse_matrix(gsr_kde_night_light *self, gsr_night #else /* GSR_DBUS */ -#include #include gsr_kde_night_light* gsr_kde_night_light_create(void) { diff --git a/src/library_loader.c b/src/library_loader.c index 37a1767..8b2e8a1 100644 --- a/src/library_loader.c +++ b/src/library_loader.c @@ -3,7 +3,6 @@ #include #include -#include void* dlsym_print_fail(void *handle, const char *name, bool required) { dlerror(); diff --git a/src/plugins.c b/src/plugins.c index 7686683..5b9fd8c 100644 --- a/src/plugins.c +++ b/src/plugins.c @@ -1,7 +1,6 @@ #include "../include/plugins.h" #include "../include/log.h" #include "../include/utils.h" -#include #include #include #include diff --git a/src/recorder/audio_codec.c b/src/recorder/audio_codec.c index 1b2d6db..e8f89e7 100644 --- a/src/recorder/audio_codec.c +++ b/src/recorder/audio_codec.c @@ -4,7 +4,6 @@ #include #include -#include #include diff --git a/src/recorder/codec_select.c b/src/recorder/codec_select.c index d3286ec..48af0b2 100644 --- a/src/recorder/codec_select.c +++ b/src/recorder/codec_select.c @@ -12,7 +12,6 @@ #include #include -#include gsr_video_encoder* create_video_encoder(gsr_egl *egl, const gsr_recorder_settings *settings) { const gsr_color_depth color_depth = video_codec_to_bit_depth(settings->video_codec); diff --git a/src/recorder/recording_clock.c b/src/recorder/recording_clock.c index cb23d56..5c9aabb 100644 --- a/src/recorder/recording_clock.c +++ b/src/recorder/recording_clock.c @@ -3,7 +3,6 @@ #include "../../include/log.h" #include -#include #include struct gsr_recording_clock { diff --git a/src/recorder/video_codec.c b/src/recorder/video_codec.c index 1713e77..a21e4ec 100644 --- a/src/recorder/video_codec.c +++ b/src/recorder/video_codec.c @@ -3,7 +3,6 @@ #include "../../include/log.h" #include -#include #include diff --git a/src/replay_buffer/replay_buffer.c b/src/replay_buffer/replay_buffer.c index 56549ee..08ed1f1 100644 --- a/src/replay_buffer/replay_buffer.c +++ b/src/replay_buffer/replay_buffer.c @@ -3,8 +3,6 @@ #include "../../include/replay_buffer/replay_buffer_disk.h" #include -#include -#include gsr_replay_buffer* gsr_replay_buffer_create(gsr_replay_storage replay_storage, const char *replay_directory, double replay_buffer_time, size_t replay_buffer_num_packets) { gsr_replay_buffer *replay_buffer = NULL; diff --git a/src/replay_buffer/replay_buffer_disk.c b/src/replay_buffer/replay_buffer_disk.c index 9a0d247..ffc59cc 100644 --- a/src/replay_buffer/replay_buffer_disk.c +++ b/src/replay_buffer/replay_buffer_disk.c @@ -3,7 +3,6 @@ #include "../../include/utils.h" #include -#include #include #include #include diff --git a/src/replay_buffer/replay_buffer_ram.c b/src/replay_buffer/replay_buffer_ram.c index c5df328..5508a12 100644 --- a/src/replay_buffer/replay_buffer_ram.c +++ b/src/replay_buffer/replay_buffer_ram.c @@ -2,7 +2,6 @@ #include "../../include/utils.h" #include -#include #include #include diff --git a/src/shader.c b/src/shader.c index 17315dc..47e152a 100644 --- a/src/shader.c +++ b/src/shader.c @@ -1,7 +1,6 @@ #include "../include/shader.h" #include "../include/log.h" #include "../include/egl.h" -#include #include static bool print_compile_errors = false; diff --git a/src/sound.c b/src/sound.c index a01718b..2d5d4c2 100644 --- a/src/sound.c +++ b/src/sound.c @@ -5,7 +5,6 @@ #include #include #include -#include #include #include diff --git a/src/window/x11.c b/src/window/x11.c index b9b6cbf..2ae62c8 100644 --- a/src/window/x11.c +++ b/src/window/x11.c @@ -5,7 +5,6 @@ #include "../../include/defs.h" #include "../../include/utils.h" -#include #include #include #include -- cgit v1.2.3 From 68d5c511d105650c9d0a5e920f77b0f19fabbfe0 Mon Sep 17 00:00:00 2001 From: dec05eba Date: Sat, 1 Aug 2026 23:46:13 +0200 Subject: Fix video size becoming 0x0 when every capture source size is a percentage The video size is the area that the capture sources cover, but a capture source with a percentage size is relative to the video size and can't be used to calculate it. When there is no other capture source to calculate the video size from, use the size of the capture sources themselves. --- src/recorder/capture_setup.c | 92 +++++++++++++++++++++++++++++--------------- 1 file changed, 60 insertions(+), 32 deletions(-) diff --git a/src/recorder/capture_setup.c b/src/recorder/capture_setup.c index 93dbe37..9fced21 100644 --- a/src/recorder/capture_setup.c +++ b/src/recorder/capture_setup.c @@ -405,6 +405,65 @@ static gsr_capture* create_capture_impl(const gsr_recorder_settings *settings, g return capture; } +/* The size and position of a capture source can be relative to the video size, in which case it can't be used to calculate the video size */ +static bool video_source_size_is_relative_to_video_size(const gsr_video_source *self) { + return self->capture_source->pos.x_type == VVEC2I_TYPE_SCALAR || self->capture_source->pos.y_type == VVEC2I_TYPE_SCALAR + || (self->capture_source->size.x_type == VVEC2I_TYPE_SCALAR && self->capture_source->size.x != 100) + || (self->capture_source->size.y_type == VVEC2I_TYPE_SCALAR && self->capture_source->size.y != 100); +} + +/* The video size is the area that all capture sources cover */ +static vec2i video_sources_get_total_size(const gsr_video_sources *self) { + vec2i start_pos = {99999, 99999}; + vec2i end_pos = {-99999, -99999}; + for(size_t i = 0; i < self->num_items; ++i) { + const gsr_video_source *video_source = &self->items[i]; + if(video_source_size_is_relative_to_video_size(video_source)) + continue; + + const vec2i video_source_start_pos = { + video_source->capture_source->pos.x, + video_source->capture_source->pos.y + }; + + const vec2i video_source_end_pos = { + video_source_start_pos.x + video_source->metadata.video_size.x, + video_source_start_pos.y + video_source->metadata.video_size.y + }; + + if(video_source_start_pos.x < start_pos.x) + start_pos.x = video_source_start_pos.x; + if(video_source_start_pos.y < start_pos.y) + start_pos.y = video_source_start_pos.y; + + if(video_source_end_pos.x > end_pos.x) + end_pos.x = video_source_end_pos.x; + if(video_source_end_pos.y > end_pos.y) + end_pos.y = video_source_end_pos.y; + } + + /* Every capture source is relative to the video size, so use the size of the capture sources themselves as the video size */ + if(end_pos.x <= start_pos.x || end_pos.y <= start_pos.y) { + start_pos = (vec2i){0, 0}; + end_pos = (vec2i){0, 0}; + for(size_t i = 0; i < self->num_items; ++i) { + const vec2i capture_size = self->items[i].metadata.video_size; + if(capture_size.x > end_pos.x) + end_pos.x = capture_size.x; + if(capture_size.y > end_pos.y) + end_pos.y = capture_size.y; + } + } + + vec2i video_size = { end_pos.x - start_pos.x, end_pos.y - start_pos.y }; + if(video_size.x < 0) + video_size.x = 0; + if(video_size.y < 0) + video_size.y = 0; + + return video_size; +} + int gsr_video_sources_create(gsr_video_sources *self, const gsr_recorder_settings *settings, gsr_egl *egl, gsr_capture_deps *deps, bool prefer_ximage, gsr_capture_sources *capture_sources, vec2i *video_size) { memset(self, 0, sizeof(*self)); if(capture_sources->num_items == 0) @@ -446,38 +505,7 @@ int gsr_video_sources_create(gsr_video_sources *self, const gsr_recorder_setting } } - vec2i start_pos = {99999, 99999}; - vec2i end_pos = {-99999, -99999}; - for(size_t i = 0; i < self->num_items; ++i) { - const gsr_video_source *video_source = &self->items[i]; - /* TODO: Skip scalar positions for now, but this should be handled in a better way. - Maybe handle scalars at the next loop by multiplying video size by the scalar. */ - if(video_source->capture_source->pos.x_type == VVEC2I_TYPE_SCALAR || video_source->capture_source->pos.y_type == VVEC2I_TYPE_SCALAR - || (video_source->capture_source->size.x_type == VVEC2I_TYPE_SCALAR && video_source->capture_source->size.x != 100) - || (video_source->capture_source->size.y_type == VVEC2I_TYPE_SCALAR && video_source->capture_source->size.y != 100)) - { - continue; - } - const vec2i video_source_start_pos = {video_source->capture_source->pos.x, video_source->capture_source->pos.y}; - const vec2i video_source_end_pos = {video_source_start_pos.x + video_source->metadata.video_size.x, video_source_start_pos.y + video_source->metadata.video_size.y}; - - if(video_source_start_pos.x < start_pos.x) - start_pos.x = video_source_start_pos.x; - if(video_source_start_pos.y < start_pos.y) - start_pos.y = video_source_start_pos.y; - - if(video_source_end_pos.x > end_pos.x) - end_pos.x = video_source_end_pos.x; - if(video_source_end_pos.y > end_pos.y) - end_pos.y = video_source_end_pos.y; - } - - video_size->x = end_pos.x - start_pos.x; - video_size->y = end_pos.y - start_pos.y; - if(video_size->x < 0) - video_size->x = 0; - if(video_size->y < 0) - video_size->y = 0; + *video_size = video_sources_get_total_size(self); for(size_t i = 0; i < self->num_items; ++i) { self->items[i].metadata.video_size = *video_size; -- cgit v1.2.3 From 06be2c8a12cd021083670d304a72f93907c8b22e Mon Sep 17 00:00:00 2001 From: dec05eba Date: Sun, 2 Aug 2026 00:04:45 +0200 Subject: Add resource cleanup on all paths The recorder, screenshot and cli now free everything they create, also when setup fails. Adds gsr_windowing_deinit, implements the missing gsr_window_destroy and destroys the pipewire registry proxy. --- include/recorder/windowing.h | 1 + src/cli/commands.c | 40 ++++----- src/cli/main.c | 200 +++++++++++++++++++++++++------------------ src/pipewire_audio.c | 5 ++ src/recorder/recorder.c | 140 +++++++++++++++--------------- src/recorder/screenshot.c | 18 ++-- src/recorder/windowing.c | 18 ++++ src/window/window.c | 5 +- 8 files changed, 237 insertions(+), 190 deletions(-) diff --git a/include/recorder/windowing.h b/include/recorder/windowing.h index 6bc5f5f..630f3fe 100644 --- a/include/recorder/windowing.h +++ b/include/recorder/windowing.h @@ -25,6 +25,7 @@ typedef struct { int gsr_windowing_init(gsr_windowing *self, const gsr_windowing_params *params); /* Returns a |gsr_error| value. Loads opengl and finds the drm card to use */ int gsr_windowing_load_egl(gsr_windowing *self, const gsr_windowing_params *params); +void gsr_windowing_deinit(gsr_windowing *self); bool gsr_windowing_is_wayland(const gsr_windowing *self); bool gsr_windowing_is_using_prime_run(void); diff --git a/src/cli/commands.c b/src/cli/commands.c index 38342ec..12de13b 100644 --- a/src/cli/commands.c +++ b/src/cli/commands.c @@ -238,8 +238,12 @@ int info_command(void *userdata) { const gsr_windowing_params windowing_params = {false, false, false}; if(gsr_windowing_init(&windowing, &windowing_params) != GSR_ERROR_OK) return 1; - if(gsr_windowing_load_egl(&windowing, &windowing_params) != GSR_ERROR_OK) + + 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); @@ -262,13 +266,7 @@ int info_command(void *userdata) { list_supported_capture_options(windowing.window, windowing.egl.card_path, windowing.card_path_found); fflush(stdout); - - // Not needed as this will just slow down shutdown - //gsr_egl_unload(&egl); - //gsr_window_destroy(&window); - //if(dpy) - // XCloseDisplay(dpy); - + gsr_windowing_deinit(&windowing); return 0; } @@ -328,8 +326,11 @@ int list_capture_options_command(const char *card_path, void *userdata) { const gsr_windowing_params windowing_params = {false, false, false}; 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) + + 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); @@ -337,13 +338,7 @@ int list_capture_options_command(const char *card_path, void *userdata) { list_supported_capture_options(windowing.window, windowing.egl.card_path, windowing.card_path_found); fflush(stdout); - - // Not needed as this will just slow down shutdown - //gsr_egl_unload(&egl); - //gsr_window_destroy(&window); - //if(dpy) - // XCloseDisplay(dpy); - + gsr_windowing_deinit(&windowing); return 0; } @@ -353,19 +348,16 @@ int list_monitors_command(void *userdata) { const gsr_windowing_params windowing_params = {false, false, false}; if(gsr_windowing_init(&windowing, &windowing_params) != GSR_ERROR_OK) return 1; - if(gsr_windowing_load_egl(&windowing, &windowing_params) != GSR_ERROR_OK) + + 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); - - // Not needed as this will just slow down shutdown - //gsr_egl_unload(&egl); - //gsr_window_destroy(&window); - //if(dpy) - // XCloseDisplay(dpy); - + gsr_windowing_deinit(&windowing); return 0; } diff --git a/src/cli/main.c b/src/cli/main.c index 563ab19..9d5f7e2 100644 --- a/src/cli/main.c +++ b/src/cli/main.c @@ -405,99 +405,75 @@ static int record(args_parser *arg_parser, gsr_windowing *windowing, gsr_capture return run_result; } -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; +static int run(args_parser *arg_parser) { + int exit_code = 0; - args_parser arg_parser; - 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(1); - case ARGS_PARSE_RESULT_COMMAND_HANDLED: - _exit(command_exit_code); - case ARGS_PARSE_RESULT_OK: - break; - } + 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; + memset(&audio_input_tracks, 0, sizeof(audio_input_tracks)); + memset(&app_audio_names, 0, sizeof(app_audio_names)); + memset(&windowing, 0, sizeof(windowing)); + gsr_capture_deps_init(&capture_deps); - 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); + 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; } - gsr_capture_sources capture_sources; - 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(gsr_error_to_exit_code(parse_capture_sources_result)); - 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(1); + 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(gsr_error_to_exit_code(validate_args_result)); + 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; + } - gsr_audio_input_tracks audio_input_tracks; - const int parse_audio_inputs_result = parse_audio_inputs(&arg_parser, &audio_input_tracks); - if(parse_audio_inputs_result != GSR_ERROR_OK) - _exit(gsr_error_to_exit_code(parse_audio_inputs_result)); + 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); - gsr_app_audio_names app_audio_names; - memset(&app_audio_names, 0, sizeof(app_audio_names)); - #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(gsr_error_to_exit_code(app_audio_result)); + 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(2); + 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); - gsr_app_audio_names_deinit(&app_audio_names); - if(validate_app_audio_result != GSR_ERROR_OK) - _exit(gsr_error_to_exit_code(validate_app_audio_result)); + if(validate_app_audio_result != GSR_ERROR_OK) { + exit_code = gsr_error_to_exit_code(validate_app_audio_result); + goto done; + } - gsr_windowing windowing; 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.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(1); + 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()) { @@ -505,56 +481,114 @@ int main(int argc, char **argv) { gsr_windowing_disable_prime_run(); } - if(video_codec_is_hdr(arg_parser.settings.video_codec)) { + 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); + 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(1); + 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); + 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(1); + 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(2); + exit_code = 2; + goto done; } - gsr_capture_deps capture_deps; - gsr_capture_deps_init(&capture_deps); - gsr_capture_deps_init_cursor(&capture_deps, &windowing.egl, arg_parser.settings.record_cursor); + gsr_capture_deps_init_cursor(&capture_deps, &windowing.egl, arg_parser->settings.record_cursor); - int result = GSR_ERROR_OK; gsr_image_format image_format; - if(get_image_format_from_filename(arg_parser.settings.filename, &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(1); + exit_code = 1; + goto done; } - result = take_screenshot(&arg_parser, &windowing, &capture_deps, &capture_sources, image_format); + exit_code = gsr_error_to_exit_code(take_screenshot(arg_parser, &windowing, &capture_deps, &capture_sources, image_format)); } else { - result = record(&arg_parser, &windowing, &capture_deps, &capture_sources, &audio_input_tracks); + exit_code = gsr_error_to_exit_code(record(arg_parser, &windowing, &capture_deps, &capture_sources, &audio_input_tracks)); } + done: 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(gsr_error_to_exit_code(result)); + _exit(exit_code); } diff --git a/src/pipewire_audio.c b/src/pipewire_audio.c index 73192a9..8f40695 100644 --- a/src/pipewire_audio.c +++ b/src/pipewire_audio.c @@ -797,6 +797,11 @@ void gsr_pipewire_audio_deinit(gsr_pipewire_audio *self) { spa_hook_remove(&self->registry_listener); spa_hook_remove(&self->core_listener); + if(self->registry) { + pw_proxy_destroy((struct pw_proxy*)self->registry); + self->registry = NULL; + } + if(self->core) { pw_core_disconnect(self->core); self->core = NULL; diff --git a/src/recorder/recorder.c b/src/recorder/recorder.c index 7ace350..2fc62d5 100644 --- a/src/recorder/recorder.c +++ b/src/recorder/recorder.c @@ -273,106 +273,97 @@ static int recorder_setup_video_encoder(gsr_recorder *self) { return GSR_ERROR_OK; } -static int recorder_setup_audio(gsr_recorder *self) { - if(gsr_audio_capture_init(&self->audio_capture, &self->encoder, self->recording_clock, &self->running) != GSR_ERROR_OK) { +static int recorder_setup_audio_track(gsr_recorder *self, const gsr_merged_audio_inputs *merged_audio_inputs, int audio_stream_index, gsr_audio_track *audio_track) { + const bool use_amix = gsr_audio_inputs_should_use_amix(merged_audio_inputs); + + memset(audio_track, 0, sizeof(*audio_track)); + audio_track->stream_index = audio_stream_index; + audio_track->codec_context = create_audio_codec_context(self->settings.fps, self->settings.audio_codec, use_amix, self->settings.audio_bitrate); + if(!audio_track->codec_context) return GSR_ERROR_GENERIC; + AVStream *audio_stream = NULL; + if(!self->settings.is_replaying) { + audio_stream = create_stream(self->av_format_context, audio_track->codec_context); + if(!audio_stream) + return GSR_ERROR_GENERIC; + + if(gsr_encoder_add_recording_destination(&self->encoder, audio_track->codec_context, self->av_format_context, audio_stream, 0) == (size_t)-1) + gsr_log(GSR_LOG_LEVEL_ERROR, "added too many audio sources"); } - - int audio_stream_index = GSR_VIDEO_STREAM_INDEX + 1; - for(size_t audio_track_index = 0; audio_track_index < self->audio_input_tracks->num_items; ++audio_track_index) { - const gsr_merged_audio_inputs *merged_audio_inputs = &self->audio_input_tracks->items[audio_track_index]; - const bool use_amix = gsr_audio_inputs_should_use_amix(merged_audio_inputs); - AVCodecContext *audio_codec_context = create_audio_codec_context(self->settings.fps, self->settings.audio_codec, use_amix, self->settings.audio_bitrate); - if(!audio_codec_context) { - return GSR_ERROR_GENERIC; - } + snprintf(audio_track->name, sizeof(audio_track->name), "%s", merged_audio_inputs->track_name); + if(audio_stream && audio_track->name[0] != '\0' && !self->settings.exclude_metadata) + av_dict_set(&audio_stream->metadata, "title", audio_track->name, 0); - AVStream *audio_stream = NULL; - if(!self->settings.is_replaying) { - audio_stream = create_stream(self->av_format_context, audio_codec_context); - if(gsr_encoder_add_recording_destination(&self->encoder, audio_codec_context, self->av_format_context, audio_stream, 0) == (size_t)-1) - gsr_log(GSR_LOG_LEVEL_ERROR, "added too many audio sources"); - } + if(!open_audio(audio_track->codec_context, self->settings.ffmpeg_audio_opts)) + return GSR_ERROR_GENERIC; - if(audio_stream && merged_audio_inputs->track_name[0] != '\0' && !self->settings.exclude_metadata) - av_dict_set(&audio_stream->metadata, "title", merged_audio_inputs->track_name, 0); + if(audio_stream) + avcodec_parameters_from_context(audio_stream->codecpar, audio_track->codec_context); - if(!open_audio(audio_codec_context, self->settings.ffmpeg_audio_opts)) { - return GSR_ERROR_GENERIC; + #if LIBAVCODEC_VERSION_MAJOR < 60 + const int num_channels = audio_track->codec_context->channels; + #else + const int num_channels = audio_track->codec_context->ch_layout.nb_channels; + #endif + AVFilterContext *src_filter_ctx[GSR_MAX_AUDIO_SOURCES_PER_TRACK]; + if(use_amix) { + if(merged_audio_inputs->num_items > GSR_MAX_AUDIO_SOURCES_PER_TRACK) { + gsr_log(GSR_LOG_LEVEL_ERROR, "too many audio sources for one audio track, the maximum is %d", GSR_MAX_AUDIO_SOURCES_PER_TRACK); + return GSR_ERROR_GENERIC; } - if(audio_stream) - avcodec_parameters_from_context(audio_stream->codecpar, audio_codec_context); - - #if LIBAVCODEC_VERSION_MAJOR < 60 - const int num_channels = audio_codec_context->channels; - #else - const int num_channels = audio_codec_context->ch_layout.nb_channels; - #endif - - //audio_frame->sample_rate = audio_codec_context->sample_rate; - - AVFilterContext *src_filter_ctx[GSR_MAX_AUDIO_SOURCES_PER_TRACK]; - AVFilterGraph *graph = NULL; - AVFilterContext *sink = NULL; - if(use_amix) { - if(merged_audio_inputs->num_items > GSR_MAX_AUDIO_SOURCES_PER_TRACK) { - gsr_log(GSR_LOG_LEVEL_ERROR, "too many audio sources for one audio track, the maximum is %d", GSR_MAX_AUDIO_SOURCES_PER_TRACK); - return GSR_ERROR_GENERIC; - } - if(gsr_audio_init_filter_graph(audio_codec_context, &graph, &sink, src_filter_ctx, merged_audio_inputs->num_items) < 0) { - gsr_log(GSR_LOG_LEVEL_ERROR, "failed to create audio filter"); - return GSR_ERROR_GENERIC; - } + if(gsr_audio_init_filter_graph(audio_track->codec_context, &audio_track->graph, &audio_track->sink, src_filter_ctx, merged_audio_inputs->num_items) < 0) { + gsr_log(GSR_LOG_LEVEL_ERROR, "failed to create audio filter"); + return GSR_ERROR_GENERIC; } + } - // TODO: Cleanup above - - const double audio_fps = (double)audio_codec_context->sample_rate / (double)audio_codec_context->frame_size; - const double timeout_sec = 1000.0 / audio_fps / 1000.0; + const double audio_fps = (double)audio_track->codec_context->sample_rate / (double)audio_track->codec_context->frame_size; + const double timeout_sec = 1000.0 / audio_fps / 1000.0; - const double audio_startup_time_seconds = self->force_no_audio_offset ? 0 : audio_codec_get_desired_delay(self->settings.audio_codec, self->settings.fps);// * ((double)audio_codec_context->frame_size / 1024.0); - const double num_audio_frames_shift = audio_startup_time_seconds / timeout_sec; + const double audio_startup_time_seconds = self->force_no_audio_offset ? 0 : audio_codec_get_desired_delay(self->settings.audio_codec, self->settings.fps); + const double num_audio_frames_shift = audio_startup_time_seconds / timeout_sec; + audio_track->pts = -audio_track->codec_context->frame_size * num_audio_frames_shift; - gsr_audio_track audio_track; - memset(&audio_track, 0, sizeof(audio_track)); - snprintf(audio_track.name, sizeof(audio_track.name), "%s", merged_audio_inputs->track_name); - audio_track.codec_context = audio_codec_context; - audio_track.graph = graph; - audio_track.sink = sink; - audio_track.stream_index = audio_stream_index; - audio_track.pts = -audio_codec_context->frame_size * num_audio_frames_shift; - - int audio_track_result = GSR_ERROR_OK; - if(gsr_audio_inputs_has_app_audio(merged_audio_inputs)) { - assert(!use_amix); + if(gsr_audio_inputs_has_app_audio(merged_audio_inputs)) { + assert(!use_amix); #ifdef GSR_APP_AUDIO - audio_track_result = gsr_audio_track_init_application_input(&audio_track, merged_audio_inputs, audio_codec_context, num_channels, num_audio_frames_shift, self->pipewire_audio); + return gsr_audio_track_init_application_input(audio_track, merged_audio_inputs, audio_track->codec_context, num_channels, num_audio_frames_shift, self->pipewire_audio); +#else + return GSR_ERROR_UNSUPPORTED; #endif - } else { - audio_track_result = gsr_audio_track_init_device_inputs(&audio_track, merged_audio_inputs, audio_codec_context, num_channels, num_audio_frames_shift, src_filter_ctx, use_amix); - } + } + + return gsr_audio_track_init_device_inputs(audio_track, merged_audio_inputs, audio_track->codec_context, num_channels, num_audio_frames_shift, src_filter_ctx, use_amix); +} + +static int recorder_setup_audio(gsr_recorder *self) { + if(gsr_audio_capture_init(&self->audio_capture, &self->encoder, self->recording_clock, &self->running) != GSR_ERROR_OK) + return GSR_ERROR_GENERIC; + int audio_stream_index = GSR_VIDEO_STREAM_INDEX + 1; + for(size_t i = 0; i < self->audio_input_tracks->num_items; ++i) { + gsr_audio_track audio_track; + const int audio_track_result = recorder_setup_audio_track(self, &self->audio_input_tracks->items[i], audio_stream_index, &audio_track); if(audio_track_result != GSR_ERROR_OK) { + gsr_audio_track_deinit(&audio_track); return audio_track_result; - } if(!gsr_audio_capture_add_track(&self->audio_capture, &audio_track)) { + gsr_audio_track_deinit(&audio_track); return GSR_ERROR_GENERIC; - } + ++audio_stream_index; - if(audio_codec_context->frame_size > self->audio_max_frame_size) - self->audio_max_frame_size = audio_codec_context->frame_size; + if(audio_track.codec_context->frame_size > self->audio_max_frame_size) + self->audio_max_frame_size = audio_track.codec_context->frame_size; } - //av_dump_format(self->av_format_context, 0, filename, 1); - return GSR_ERROR_OK; } @@ -768,8 +759,11 @@ int gsr_recorder_run(gsr_recorder *self) { gsr_recording_clock_start(self->recording_clock); self->record_start_time = gsr_recording_clock_get_start_time(self->recording_clock); - if(gsr_audio_capture_start(&self->audio_capture, self->audio_max_frame_size, self->uses_amix) != GSR_ERROR_OK) + if(gsr_audio_capture_start(&self->audio_capture, self->audio_max_frame_size, self->uses_amix) != GSR_ERROR_OK) { + /* The audio threads that did start have to stop before they can be joined */ + self->running = 0; return GSR_ERROR_GENERIC; + } // Set update_fps to 24 to test if duplicate/delayed frames cause video/audio desync or too fast/slow video. //const double update_fps = fps + 190; diff --git a/src/recorder/screenshot.c b/src/recorder/screenshot.c index 45155b4..7611270 100644 --- a/src/recorder/screenshot.c +++ b/src/recorder/screenshot.c @@ -88,12 +88,15 @@ int gsr_screenshot_take(const gsr_screenshot_params *params) { return load_plugins_result; } + int result = GSR_ERROR_GENERIC; gsr_image_writer image_writer; + memset(&image_writer, 0, sizeof(image_writer)); + gsr_color_conversion color_conversion; + memset(&color_conversion, 0, sizeof(color_conversion)); + if(!gsr_image_writer_init_opengl(&image_writer, egl, video_size.x, video_size.y)) { gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_screenshot_take: gsr_image_write_gl_init failed"); - gsr_plugins_deinit(&plugins); - gsr_video_sources_deinit(video_sources); - return GSR_ERROR_GENERIC; + goto done; } gsr_color_conversion_params color_conversion_params; @@ -107,13 +110,9 @@ int gsr_screenshot_take(const gsr_screenshot_params *params) { color_conversion_params.num_destination_textures = 1; color_conversion_params.destination_color = GSR_DESTINATION_COLOR_RGB; - gsr_color_conversion color_conversion; if(gsr_color_conversion_init(&color_conversion, &color_conversion_params) != 0) { gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_screenshot_take: failed to create color conversion"); - gsr_image_writer_deinit(&image_writer); - gsr_plugins_deinit(&plugins); - gsr_video_sources_deinit(video_sources); - return GSR_ERROR_GENERIC; + goto done; } gsr_color_conversion_clear(&color_conversion); @@ -190,7 +189,7 @@ int gsr_screenshot_take(const gsr_screenshot_params *params) { gsr_egl_swap_buffers(egl); - int result = should_stop_error ? GSR_ERROR_CAPTURE_FAILED : GSR_ERROR_OK; + result = should_stop_error ? GSR_ERROR_CAPTURE_FAILED : GSR_ERROR_OK; if(!should_stop_error) { if(!gsr_image_writer_write_to_file(&image_writer, settings->filename, params->image_format, image_quality)) { gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_screenshot_take: failed to write opengl texture to image output file %s", settings->filename); @@ -201,6 +200,7 @@ int gsr_screenshot_take(const gsr_screenshot_params *params) { params->screenshot_saved(settings->filename, params->userdata); } + done: gsr_color_conversion_deinit(&color_conversion); gsr_plugins_deinit(&plugins); gsr_image_writer_deinit(&image_writer); diff --git a/src/recorder/windowing.c b/src/recorder/windowing.c index 6671898..0bd1de6 100644 --- a/src/recorder/windowing.c +++ b/src/recorder/windowing.c @@ -117,6 +117,24 @@ int gsr_windowing_load_egl(gsr_windowing *self, const gsr_windowing_params *para return GSR_ERROR_OK; } +void gsr_windowing_deinit(gsr_windowing *self) { + if(self->egl_loaded) { + gsr_egl_unload(&self->egl); + self->egl_loaded = false; + } + + if(self->window) { + gsr_window_destroy(self->window); + self->window = NULL; + } + + if(self->display) { + /* TODO: XCloseDisplay causes a crash, why? maybe some other library dlclose xlib and that also happened to unload this??? */ + //XCloseDisplay(self->display); + self->display = NULL; + } +} + bool gsr_windowing_is_wayland(const gsr_windowing *self) { return gsr_window_get_display_server(self->window) == GSR_DISPLAY_SERVER_WAYLAND; } diff --git a/src/window/window.c b/src/window/window.c index 5ede9fd..460ff7f 100644 --- a/src/window/window.c +++ b/src/window/window.c @@ -1,7 +1,10 @@ #include "../../include/window/window.h" #include -void gsr_window_destroy(gsr_window *self); +void gsr_window_destroy(gsr_window *self) { + if(self) + self->destroy(self); +} bool gsr_window_process_event(gsr_window *self) { return self->process_event(self); -- cgit v1.2.3 From 208c907c355689c0432966f0d9e32ffce9d55882 Mon Sep 17 00:00:00 2001 From: dec05eba Date: Sun, 2 Aug 2026 00:17:18 +0200 Subject: Fix indentation and spacing of the code moved out of main.cpp --- include/ffmpeg_utils.h | 8 -- include/log.h | 8 -- include/recorder/error.h | 8 -- include/recorder/recording_clock.h | 8 -- include/recorder/settings.h | 8 -- include/sound.h | 8 -- src/recorder/recorder.c | 278 ++++++++++++++++++------------------- src/recorder/screenshot.c | 8 +- 8 files changed, 143 insertions(+), 191 deletions(-) diff --git a/include/ffmpeg_utils.h b/include/ffmpeg_utils.h index 373122c..35524c2 100644 --- a/include/ffmpeg_utils.h +++ b/include/ffmpeg_utils.h @@ -1,14 +1,6 @@ #ifndef GSR_FFMPEG_UTILS_H #define GSR_FFMPEG_UTILS_H -#ifdef __cplusplus -extern "C" { -#endif - const char* gsr_av_error_to_string(int err); -#ifdef __cplusplus -} -#endif - #endif /* GSR_FFMPEG_UTILS_H */ diff --git a/include/log.h b/include/log.h index 71be585..e92ac99 100644 --- a/include/log.h +++ b/include/log.h @@ -1,10 +1,6 @@ #ifndef GSR_LOG_H #define GSR_LOG_H -#ifdef __cplusplus -extern "C" { -#endif - typedef enum { GSR_LOG_LEVEL_DEBUG, GSR_LOG_LEVEL_INFO, @@ -21,8 +17,4 @@ void gsr_log_set_level(gsr_log_level level); /* A NULL |handler| restores the default handler which prints "gsr : " to stderr */ void gsr_log_set_handler(gsr_log_handler handler, void *userdata); -#ifdef __cplusplus -} -#endif - #endif /* GSR_LOG_H */ diff --git a/include/recorder/error.h b/include/recorder/error.h index cfa83cd..ba2ae1d 100644 --- a/include/recorder/error.h +++ b/include/recorder/error.h @@ -1,10 +1,6 @@ #ifndef GSR_RECORDER_ERROR_H #define GSR_RECORDER_ERROR_H -#ifdef __cplusplus -extern "C" { -#endif - /* The negated value of each error is the exit code that gpu-screen-recorder exits with */ typedef enum { GSR_ERROR_OK = 0, @@ -24,8 +20,4 @@ static inline int gsr_error_to_exit_code(int error) { return error < 0 ? -error : 0; } -#ifdef __cplusplus -} -#endif - #endif /* GSR_RECORDER_ERROR_H */ diff --git a/include/recorder/recording_clock.h b/include/recorder/recording_clock.h index 4e302b6..b99e936 100644 --- a/include/recorder/recording_clock.h +++ b/include/recorder/recording_clock.h @@ -3,10 +3,6 @@ #include -#ifdef __cplusplus -extern "C" { -#endif - /* Monotonic clock that excludes the time the recording has been paused. Safe to use from multiple threads */ typedef struct gsr_recording_clock gsr_recording_clock; @@ -22,8 +18,4 @@ double gsr_recording_clock_get_time(const gsr_recording_clock *self); void gsr_recording_clock_set_paused(gsr_recording_clock *self, bool paused); bool gsr_recording_clock_is_paused(const gsr_recording_clock *self); -#ifdef __cplusplus -} -#endif - #endif /* GSR_RECORDER_RECORDING_CLOCK_H */ diff --git a/include/recorder/settings.h b/include/recorder/settings.h index 8bb7f77..164e082 100644 --- a/include/recorder/settings.h +++ b/include/recorder/settings.h @@ -6,10 +6,6 @@ #include "../defs.h" #include "../vec2.h" -#ifdef __cplusplus -extern "C" { -#endif - typedef struct { gsr_video_encoder_hardware video_encoder; gsr_pixel_format pixel_format; @@ -56,8 +52,4 @@ typedef struct { vec2i region_position; } gsr_recorder_settings; -#ifdef __cplusplus -} -#endif - #endif /* GSR_RECORDER_SETTINGS_H */ diff --git a/include/sound.h b/include/sound.h index 1ec8940..eaa1236 100644 --- a/include/sound.h +++ b/include/sound.h @@ -21,10 +21,6 @@ #include #include -#ifdef __cplusplus -extern "C" { -#endif - typedef struct { void *handle; unsigned int frames; @@ -77,8 +73,4 @@ void get_pulseaudio_inputs(gsr_audio_devices *audio_devices); void gsr_audio_devices_deinit(gsr_audio_devices *self); bool pulseaudio_server_is_pipewire(void); -#ifdef __cplusplus -} -#endif - #endif /* GSR_SOUND_H */ diff --git a/src/recorder/recorder.c b/src/recorder/recorder.c index 2fc62d5..3b4379f 100644 --- a/src/recorder/recorder.c +++ b/src/recorder/recorder.c @@ -434,189 +434,190 @@ gsr_recorder* gsr_recorder_create(const gsr_recorder_params *params, const gsr_r gsr_recorder_destroy(self); return NULL; } -static void recorder_process_events(gsr_recorder *self) { -while(gsr_window_process_event(self->window)) { - if(self->capture_deps->x11_cursor_display && self->settings.record_cursor) - gsr_cursor_on_event(&self->capture_deps->x11_cursor, gsr_window_get_event_data(self->window)); - gsr_damage_on_event(&self->damage, gsr_window_get_event_data(self->window)); - for(size_t video_source_index = 0; video_source_index < self->video_sources->num_items; ++video_source_index) { -gsr_video_source *video_source = &self->video_sources->items[video_source_index]; - gsr_capture_on_event(video_source->capture, self->egl); +static void recorder_process_events(gsr_recorder *self) { + while(gsr_window_process_event(self->window)) { + if(self->capture_deps->x11_cursor_display && self->settings.record_cursor) + gsr_cursor_on_event(&self->capture_deps->x11_cursor, gsr_window_get_event_data(self->window)); + + gsr_damage_on_event(&self->damage, gsr_window_get_event_data(self->window)); + for(size_t video_source_index = 0; video_source_index < self->video_sources->num_items; ++video_source_index) { + gsr_video_source *video_source = &self->video_sources->items[video_source_index]; + gsr_capture_on_event(video_source->capture, self->egl); + } } -} -if(self->capture_deps->x11_cursor_display && self->settings.record_cursor) - gsr_cursor_tick(&self->capture_deps->x11_cursor, DefaultRootWindow(self->capture_deps->x11_cursor_display)); + if(self->capture_deps->x11_cursor_display && self->settings.record_cursor) + gsr_cursor_tick(&self->capture_deps->x11_cursor, DefaultRootWindow(self->capture_deps->x11_cursor_display)); } static bool recorder_tick_video_sources(gsr_recorder *self) { -gsr_damage_tick(&self->damage); + gsr_damage_tick(&self->damage); -self->should_stop_error = false; -bool damaged = false; + self->should_stop_error = false; + bool damaged = false; -if(self->use_damage_tracking) - damaged = gsr_damage_is_damaged(&self->damage); + if(self->use_damage_tracking) + damaged = gsr_damage_is_damaged(&self->damage); -for(size_t video_source_index = 0; video_source_index < self->video_sources->num_items; ++video_source_index) { -gsr_video_source *video_source = &self->video_sources->items[video_source_index]; - gsr_capture_tick(video_source->capture); + for(size_t video_source_index = 0; video_source_index < self->video_sources->num_items; ++video_source_index) { + gsr_video_source *video_source = &self->video_sources->items[video_source_index]; + gsr_capture_tick(video_source->capture); - if(gsr_capture_should_stop(video_source->capture, &self->should_stop_error)) { - self->running = 0; - break; - } + if(gsr_capture_should_stop(video_source->capture, &self->should_stop_error)) { + self->running = 0; + break; + } + + if(video_source->capture_source->type == GSR_CAPTURE_SOURCE_TYPE_FOCUSED_WINDOW) { + assert(video_source->capture->get_window_id); + const Window damage_target_window = video_source->capture->get_window_id(video_source->capture); - if(video_source->capture_source->type == GSR_CAPTURE_SOURCE_TYPE_FOCUSED_WINDOW) { - assert(video_source->capture->get_window_id); - const Window damage_target_window = video_source->capture->get_window_id(video_source->capture); + if((int64_t)damage_target_window != video_source->capture_source->window_id) { + gsr_damage_stop_tracking_window(&self->damage, video_source->capture_source->window_id); + if(damage_target_window != 0) + gsr_damage_start_tracking_window(&self->damage, damage_target_window); + } - if((int64_t)damage_target_window != video_source->capture_source->window_id) { - gsr_damage_stop_tracking_window(&self->damage, video_source->capture_source->window_id); - if(damage_target_window != 0) - gsr_damage_start_tracking_window(&self->damage, damage_target_window); + video_source->capture_source->window_id = damage_target_window; } - video_source->capture_source->window_id = damage_target_window; + if(video_source->capture->is_damaged) + damaged |= video_source->capture->is_damaged(video_source->capture); + else if(!self->use_damage_tracking) + damaged = true; } - if(video_source->capture->is_damaged) - damaged |= video_source->capture->is_damaged(video_source->capture); - else if(!self->use_damage_tracking) - damaged = true; -} + damaged |= gsr_plugins_is_damaged(&self->plugins); -damaged |= gsr_plugins_is_damaged(&self->plugins); - -// TODO: Readd wayland sync warning when removing this -if(self->settings.framerate_mode != GSR_FRAMERATE_MODE_CONTENT) - damaged = true; + // TODO: Readd wayland sync warning when removing this + if(self->settings.framerate_mode != GSR_FRAMERATE_MODE_CONTENT) + damaged = true; -if(damaged) - ++self->damage_fps_counter; + if(damaged) + ++self->damage_fps_counter; return damaged; } static void recorder_update_fps_counters(gsr_recorder *self, bool damaged) { -++self->fps_counter; -const double time_now = clock_get_monotonic_seconds(); -//const double frame_timer_elapsed = time_now - frame_timer_start; -const double elapsed = time_now - self->fps_start_time; -if (elapsed >= 1.0) { - if(self->settings.verbose) { - fprintf(stderr, "update fps: %d, damage fps: %d\n", self->fps_counter, self->damage_fps_counter); - } - self->fps_start_time = time_now; - self->fps_counter = 0; - self->damage_fps_counter = 0; -} + ++self->fps_counter; + const double time_now = clock_get_monotonic_seconds(); + //const double frame_timer_elapsed = time_now - frame_timer_start; + const double elapsed = time_now - self->fps_start_time; + if (elapsed >= 1.0) { + if(self->settings.verbose) { + fprintf(stderr, "update fps: %d, damage fps: %d\n", self->fps_counter, self->damage_fps_counter); + } + self->fps_start_time = time_now; + self->fps_counter = 0; + self->damage_fps_counter = 0; + } } static void recorder_capture_and_encode_frame(gsr_recorder *self, bool damaged) { -const double this_video_frame_time = gsr_recording_clock_get_time(self->recording_clock); -const int64_t expected_frames = floor((this_video_frame_time - self->record_start_time) / self->target_fps); -const int64_t num_missed_frames = expected_frames - self->video_pts_counter; - -if(damaged && num_missed_frames >= 1 && !self->paused) { - // TODO: Dont do this if no damage? - self->egl->glClear(0); - - gsr_damage_clear(&self->damage); - gsr_plugins_clear_damage(&self->plugins); - gsr_capture_deps_cleanup_kms_fds(self->capture_deps); - - gsr_capture_deps_update_kms(self->capture_deps); - - bool capture_has_synchronous_task = false; - for(size_t video_source_index = 0; video_source_index < self->video_sources->num_items; ++video_source_index) { -gsr_video_source *video_source = &self->video_sources->items[video_source_index]; - if(video_source->capture->clear_damage) - video_source->capture->clear_damage(video_source->capture); - - if(video_source->capture->capture_has_synchronous_task) { - capture_has_synchronous_task = video_source->capture->capture_has_synchronous_task(video_source->capture); - if(capture_has_synchronous_task) { - self->paused = true; - gsr_recording_clock_set_paused(self->recording_clock, true); + const double this_video_frame_time = gsr_recording_clock_get_time(self->recording_clock); + const int64_t expected_frames = floor((this_video_frame_time - self->record_start_time) / self->target_fps); + const int64_t num_missed_frames = expected_frames - self->video_pts_counter; + + if(damaged && num_missed_frames >= 1 && !self->paused) { + // TODO: Dont do this if no damage? + self->egl->glClear(0); + + gsr_damage_clear(&self->damage); + gsr_plugins_clear_damage(&self->plugins); + gsr_capture_deps_cleanup_kms_fds(self->capture_deps); + + gsr_capture_deps_update_kms(self->capture_deps); + + bool capture_has_synchronous_task = false; + for(size_t video_source_index = 0; video_source_index < self->video_sources->num_items; ++video_source_index) { + gsr_video_source *video_source = &self->video_sources->items[video_source_index]; + if(video_source->capture->clear_damage) + video_source->capture->clear_damage(video_source->capture); + + if(video_source->capture->capture_has_synchronous_task) { + capture_has_synchronous_task = video_source->capture->capture_has_synchronous_task(video_source->capture); + if(capture_has_synchronous_task) { + self->paused = true; + gsr_recording_clock_set_paused(self->recording_clock, true); + } } } - } - for(size_t video_source_index = 0; video_source_index < self->video_sources->num_items; ++video_source_index) { -gsr_video_source *video_source = &self->video_sources->items[video_source_index]; - if(video_source->capture->pre_capture) - video_source->capture->pre_capture(video_source->capture, &video_source->metadata, self->output_color_conversion); - } + for(size_t video_source_index = 0; video_source_index < self->video_sources->num_items; ++video_source_index) { + gsr_video_source *video_source = &self->video_sources->items[video_source_index]; + if(video_source->capture->pre_capture) + video_source->capture->pre_capture(video_source->capture, &video_source->metadata, self->output_color_conversion); + } - if(self->output_color_conversion->schedule_clear) { - self->output_color_conversion->schedule_clear = false; - gsr_color_conversion_clear(self->output_color_conversion); - } + if(self->output_color_conversion->schedule_clear) { + self->output_color_conversion->schedule_clear = false; + gsr_color_conversion_clear(self->output_color_conversion); + } - for(size_t video_source_index = 0; video_source_index < self->video_sources->num_items; ++video_source_index) { -gsr_video_source *video_source = &self->video_sources->items[video_source_index]; - gsr_capture_capture(video_source->capture, &video_source->metadata, self->output_color_conversion); - } + for(size_t video_source_index = 0; video_source_index < self->video_sources->num_items; ++video_source_index) { + gsr_video_source *video_source = &self->video_sources->items[video_source_index]; + gsr_capture_capture(video_source->capture, &video_source->metadata, self->output_color_conversion); + } - gsr_capture_deps_cleanup_kms_fds(self->capture_deps); + gsr_capture_deps_cleanup_kms_fds(self->capture_deps); - if(self->plugins.num_plugins > 0) { - gsr_plugins_draw(&self->plugins); - gsr_color_conversion_draw(&self->color_conversion, self->plugins.texture, + if(self->plugins.num_plugins > 0) { + gsr_plugins_draw(&self->plugins); + gsr_color_conversion_draw(&self->color_conversion, self->plugins.texture, (vec2i){0, 0}, self->video_size, (vec2i){0, 0}, self->video_size, self->video_size, GSR_ROT_0, GSR_FLIP_NONE, GSR_SOURCE_COLOR_RGB, false); - } - - if(capture_has_synchronous_task) { - self->paused = false; - gsr_recording_clock_set_paused(self->recording_clock, false); - } + } - gsr_egl_swap_buffers(self->egl); - gsr_video_encoder_copy_textures_to_frame(self->video_encoder, self->video_frame, self->output_color_conversion); + if(capture_has_synchronous_task) { + self->paused = false; + gsr_recording_clock_set_paused(self->recording_clock, false); + } - for(size_t video_source_index = 0; video_source_index < self->video_sources->num_items; ++video_source_index) { -gsr_video_source *video_source = &self->video_sources->items[video_source_index]; - if(self->hdr && !self->hdr_metadata_set && !self->settings.is_replaying && add_hdr_metadata_to_video_stream(video_source->capture, self->video_stream)) - self->hdr_metadata_set = true; - } + gsr_egl_swap_buffers(self->egl); + gsr_video_encoder_copy_textures_to_frame(self->video_encoder, self->video_frame, self->output_color_conversion); - // TODO: Check if duplicate frame can be saved just by writing it with a different pts instead of sending it again - const int num_frames_to_encode = self->settings.framerate_mode == GSR_FRAMERATE_MODE_CONSTANT ? num_missed_frames : 1; - for(int i = 0; i < num_frames_to_encode; ++i) { - if(self->settings.framerate_mode == GSR_FRAMERATE_MODE_CONSTANT) { - self->video_frame->pts = self->video_pts_counter + i; - } else { - self->video_frame->pts = (this_video_frame_time - self->record_start_time) * (double)AV_TIME_BASE; - const bool same_pts = self->video_frame->pts == self->video_prev_pts; - self->video_prev_pts = self->video_frame->pts; - if(same_pts) - continue; + for(size_t video_source_index = 0; video_source_index < self->video_sources->num_items; ++video_source_index) { + gsr_video_source *video_source = &self->video_sources->items[video_source_index]; + if(self->hdr && !self->hdr_metadata_set && !self->settings.is_replaying && add_hdr_metadata_to_video_stream(video_source->capture, self->video_stream)) + self->hdr_metadata_set = true; } - if(self->force_iframe_frame) { - self->video_frame->pict_type = AV_PICTURE_TYPE_I; - } + // TODO: Check if duplicate frame can be saved just by writing it with a different pts instead of sending it again + const int num_frames_to_encode = self->settings.framerate_mode == GSR_FRAMERATE_MODE_CONSTANT ? num_missed_frames : 1; + for(int i = 0; i < num_frames_to_encode; ++i) { + if(self->settings.framerate_mode == GSR_FRAMERATE_MODE_CONSTANT) { + self->video_frame->pts = self->video_pts_counter + i; + } else { + self->video_frame->pts = (this_video_frame_time - self->record_start_time) * (double)AV_TIME_BASE; + const bool same_pts = self->video_frame->pts == self->video_prev_pts; + self->video_prev_pts = self->video_frame->pts; + if(same_pts) + continue; + } - int ret = avcodec_send_frame(self->video_codec_context, self->video_frame); - if(ret == 0) { - // TODO: Move to separate thread because this could write to network (for example when livestreaming) - gsr_encoder_receive_packets(&self->encoder, self->video_codec_context, self->video_frame->pts, GSR_VIDEO_STREAM_INDEX); - } else { - gsr_log(GSR_LOG_LEVEL_ERROR, "avcodec_send_frame failed, error: %s", gsr_av_error_to_string(ret)); - } + if(self->force_iframe_frame) { + self->video_frame->pict_type = AV_PICTURE_TYPE_I; + } - if(self->force_iframe_frame) { - self->force_iframe_frame = false; - self->video_frame->pict_type = AV_PICTURE_TYPE_NONE; + int ret = avcodec_send_frame(self->video_codec_context, self->video_frame); + if(ret == 0) { + // TODO: Move to separate thread because this could write to network (for example when livestreaming) + gsr_encoder_receive_packets(&self->encoder, self->video_codec_context, self->video_frame->pts, GSR_VIDEO_STREAM_INDEX); + } else { + gsr_log(GSR_LOG_LEVEL_ERROR, "avcodec_send_frame failed, error: %s", gsr_av_error_to_string(ret)); + } + + if(self->force_iframe_frame) { + self->force_iframe_frame = false; + self->video_frame->pict_type = AV_PICTURE_TYPE_NONE; + } } - } - self->video_pts_counter += num_missed_frames; -} + self->video_pts_counter += num_missed_frames; + } } static void recorder_apply_pause_toggle(gsr_recorder *self) { @@ -775,7 +776,6 @@ int gsr_recorder_run(gsr_recorder *self) { self->hdr_metadata_set = false; self->hdr = video_codec_is_hdr(self->settings.video_codec); - memset(&self->damage, 0, sizeof(self->damage)); if(self->settings.framerate_mode == GSR_FRAMERATE_MODE_CONTENT && gsr_capture_sources_has_damage_tracked_target(self->capture_sources)) { if(gsr_window_get_display_server(self->window) == GSR_DISPLAY_SERVER_X11) { diff --git a/src/recorder/screenshot.c b/src/recorder/screenshot.c index 7611270..47fedef 100644 --- a/src/recorder/screenshot.c +++ b/src/recorder/screenshot.c @@ -128,7 +128,7 @@ int gsr_screenshot_take(const gsr_screenshot_params *params) { gsr_cursor_on_event(&capture_deps->x11_cursor, gsr_window_get_event_data(window)); for(size_t video_source_index = 0; video_source_index < video_sources->num_items; ++video_source_index) { - gsr_video_source *video_source = &video_sources->items[video_source_index]; + gsr_video_source *video_source = &video_sources->items[video_source_index]; gsr_capture_on_event(video_source->capture, egl); } } @@ -142,7 +142,7 @@ int gsr_screenshot_take(const gsr_screenshot_params *params) { should_stop_error = false; for(size_t video_source_index = 0; video_source_index < video_sources->num_items; ++video_source_index) { - gsr_video_source *video_source = &video_sources->items[video_source_index]; + gsr_video_source *video_source = &video_sources->items[video_source_index]; gsr_capture_tick(video_source->capture); if(gsr_capture_should_stop(video_source->capture, &should_stop_error)) { break; @@ -151,7 +151,7 @@ int gsr_screenshot_take(const gsr_screenshot_params *params) { } for(size_t video_source_index = 0; video_source_index < video_sources->num_items; ++video_source_index) { - gsr_video_source *video_source = &video_sources->items[video_source_index]; + gsr_video_source *video_source = &video_sources->items[video_source_index]; if(video_source->capture->pre_capture) video_source->capture->pre_capture(video_source->capture, &video_source->metadata, output_color_conversion); } @@ -163,7 +163,7 @@ int gsr_screenshot_take(const gsr_screenshot_params *params) { bool all_sources_captured = true; for(size_t video_source_index = 0; video_source_index < video_sources->num_items; ++video_source_index) { - gsr_video_source *video_source = &video_sources->items[video_source_index]; + gsr_video_source *video_source = &video_sources->items[video_source_index]; // It can fail, for example when capturing portal and the target is a monitor that hasn't been updated. // This can also happen for example if the system suspends and the monitor to capture's framebuffer is gone, or if the target window disappeared. if(gsr_capture_capture(video_source->capture, &video_source->metadata, output_color_conversion) != 0) -- cgit v1.2.3 From b38d324c8fcb61b699a16c39fc0dcdb31ad9934b Mon Sep 17 00:00:00 2001 From: dec05eba Date: Sun, 2 Aug 2026 00:19:49 +0200 Subject: Remove unused parameter from recorder_update_fps_counters --- src/recorder/recorder.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/recorder/recorder.c b/src/recorder/recorder.c index 3b4379f..7c45594 100644 --- a/src/recorder/recorder.c +++ b/src/recorder/recorder.c @@ -500,7 +500,7 @@ static bool recorder_tick_video_sources(gsr_recorder *self) { return damaged; } -static void recorder_update_fps_counters(gsr_recorder *self, bool damaged) { +static void recorder_update_fps_counters(gsr_recorder *self) { ++self->fps_counter; const double time_now = clock_get_monotonic_seconds(); //const double frame_timer_elapsed = time_now - frame_timer_start; @@ -805,7 +805,7 @@ int gsr_recorder_run(gsr_recorder *self) { while(self->running) { recorder_process_events(self); const bool damaged = recorder_tick_video_sources(self); - recorder_update_fps_counters(self, damaged); + recorder_update_fps_counters(self); recorder_capture_and_encode_frame(self, damaged); recorder_apply_pause_toggle(self); recorder_apply_replay_recording_toggle(self); -- cgit v1.2.3 From d72113be395891f4289f0d86a32a37a99a4d5b38 Mon Sep 17 00:00:00 2001 From: dec05eba Date: Sun, 2 Aug 2026 01:11:33 +0200 Subject: Add -ipc option to control gpu-screen-recorder over a unix domain socket The socket accepts the same commands as the signal handlers, except that saving a replay takes an arbitrary number of seconds instead of only the fixed times that the signals provide. Requests and replies are newline terminated json objects and every request is replied to, parsed with the sj.h json library that was added to external. Requests are handled on a thread that only runs while the recorder exists, so the recorder control fields are now atomics, which they have to be now that another thread writes them. --- README.md | 5 +- external/sj.h | 156 +++++++++++ gpu-screen-recorder.1 | 76 ++++++ include/args_parser.h | 2 +- include/cli/ipc.h | 56 ++++ include/recorder/audio_capture.h | 6 +- include/recorder/replay_save.h | 4 +- include/recorder/screenshot.h | 4 +- meson.build | 1 + src/args_parser.c | 3 +- src/cli/ipc.c | 555 +++++++++++++++++++++++++++++++++++++++ src/cli/main.c | 81 +++++- src/recorder/audio_capture.c | 8 +- src/recorder/recorder.c | 48 ++-- src/recorder/replay_save.c | 8 +- src/recorder/screenshot.c | 6 +- 16 files changed, 970 insertions(+), 49 deletions(-) create mode 100644 external/sj.h create mode 100644 include/cli/ipc.h create mode 100644 src/cli/ipc.c diff --git a/README.md b/README.md index 953e1c1..089076e 100644 --- a/README.md +++ b/README.md @@ -138,7 +138,10 @@ This way of recording while using replay/streaming is more efficient than runnin To save a video in replay mode, you need to send signal SIGUSR1 to gpu screen recorder. You can do this by running `pkill -SIGUSR1 -f "^gpu-screen-recorder"`.\ To stop recording send SIGINT to gpu screen recorder. You can do this by running `pkill -SIGINT -f "^gpu-screen-recorder"` or pressing `Ctrl-C` in the terminal that runs gpu screen recorder. When recording a regular non-replay video this will also save the video.\ To pause/unpause recording send SIGUSR2 to gpu screen recorder. You can do this by running `pkill -SIGUSR2 -f "^gpu-screen-recorder"`. This is only applicable and useful when recording (not streaming nor replay).\ -There are more signals to control GPU Screen Recorder. Run `gpu-screen-recorder --help` to list them all (under `NOTES` section). +There are more signals to control GPU Screen Recorder. Run `gpu-screen-recorder --help` to list them all (under `NOTES` section).\ +GPU Screen Recorder can also be controlled with json messages over a unix domain socket by launching it with the `-ipc` option, for example `-ipc "$XDG_RUNTIME_DIR/gsr.sock"`. +This gives the same control as the signals, except that a replay can be saved with an arbitrary number of seconds, for example `echo '{"id":1,"name":"save-replay","data":30}' | socat - "UNIX-CONNECT:$XDG_RUNTIME_DIR/gsr.sock"`. +See the `IPC` section in the man page (`man gpu-screen-recorder`) for the full protocol. ## Simple way to run replay without gui Run the script `scripts/start-replay.sh` to start replay and then `scripts/save-replay.sh` to save a replay and `scripts/stop-replay.sh` to stop the replay. The videos are saved to `$HOME/Videos`. You can use these scripts to start replay at system startup if you add `scripts/start-replay.sh` to startup (this can be done differently depending on your desktop environment / window manager) and then go into diff --git a/external/sj.h b/external/sj.h new file mode 100644 index 0000000..60bea9e --- /dev/null +++ b/external/sj.h @@ -0,0 +1,156 @@ +// sj.h - v0.4 - rxi 2025 +// public domain - no warranty implied, use at your own risk + +#ifndef SJ_H +#define SJ_H + +#include +#include + +typedef struct { + char *data, *cur, *end; + int depth; + char *error; +} sj_Reader; + +typedef struct { + int type; + char *start, *end; + int depth; +} sj_Value; + +enum { SJ_ERROR, SJ_END, SJ_ARRAY, SJ_OBJECT, SJ_NUMBER, SJ_STRING, SJ_BOOL, SJ_NULL }; + +sj_Reader sj_reader(char *data, size_t len); +sj_Value sj_read(sj_Reader *r); +bool sj_iter_array(sj_Reader *r, sj_Value arr, sj_Value *val); +bool sj_iter_object(sj_Reader *r, sj_Value obj, sj_Value *key, sj_Value *val); +void sj_location(sj_Reader *r, int *line, int *col); + +#endif // #ifndef SJ_H + +#ifdef SJ_IMPL + + +sj_Reader sj_reader(char *data, size_t len) { + return (sj_Reader){ .data = data, .cur = data, .end = data + len }; +} + + +static bool sj__is_number_cont(char c) { + return (c >= '0' && c <= '9') + || c == 'e' || c == 'E' || c == '.' || c == '-' || c == '+'; +} + +static bool sj__is_string(char *cur, char *end, char *expect) { + while (*expect) { + if (cur == end || *cur != *expect) { + return false; + } + expect++, cur++; + } + return true; +} + + +sj_Value sj_read(sj_Reader *r) { + sj_Value res; +top: + if (r->error) { return (sj_Value){ .type = SJ_ERROR, .start = r->cur, .end = r->cur }; } + if (r->cur == r->end) { r->error = "unexpected eof"; goto top; } + res.start = r->cur; + + switch (*r->cur) { + case ' ': case '\n': case '\r': case '\t': + case ':': case ',': + r->cur++; + goto top; + + case '-': case '0': case '1': case '2': case '3': case '4': + case '5': case '6': case '7': case '8': case '9': + res.type = SJ_NUMBER; + while (r->cur != r->end && sj__is_number_cont(*r->cur)) { r->cur++; } + break; + + case '"': + res.type = SJ_STRING; + res.start = ++r->cur; + for (;;) { + if ( r->cur == r->end) { r->error = "unclosed string"; goto top; } + if (*r->cur == '"') { break; } + if (*r->cur == '\\') { r->cur++; } + if ( r->cur != r->end) { r->cur++; } + } + res.end = r->cur++; + return res; + + case '{': case '[': + res.type = (*r->cur == '{') ? SJ_OBJECT : SJ_ARRAY; + res.depth = ++r->depth; + r->cur++; + break; + + case '}': case ']': + res.type = SJ_END; + if (--r->depth < 0) { + r->error = (*r->cur == '}') ? "stray '}'" : "stray ']'"; + goto top; + } + r->cur++; + break; + + case 'n': case 't': case 'f': + res.type = (*r->cur == 'n') ? SJ_NULL : SJ_BOOL; + if (sj__is_string(r->cur, r->end, "null")) { r->cur += 4; break; } + if (sj__is_string(r->cur, r->end, "true")) { r->cur += 4; break; } + if (sj__is_string(r->cur, r->end, "false")) { r->cur += 5; break; } + // fallthrough + + default: + r->error = "unknown token"; + goto top; + } + res.end = r->cur; + return res; +} + + +static void sj__discard_until(sj_Reader *r, int depth) { + sj_Value val; + val.type = SJ_NULL; + while (r->depth != depth && val.type != SJ_ERROR) { + val = sj_read(r); + } +} + + +bool sj_iter_array(sj_Reader *r, sj_Value arr, sj_Value *val) { + sj__discard_until(r, arr.depth); + *val = sj_read(r); + if (val->type == SJ_ERROR || val->type == SJ_END) { return false; } + return true; +} + + +bool sj_iter_object(sj_Reader *r, sj_Value obj, sj_Value *key, sj_Value *val) { + sj__discard_until(r, obj.depth); + *key = sj_read(r); + if (key->type == SJ_ERROR || key->type == SJ_END) { return false; } + *val = sj_read(r); + if (val->type == SJ_END) { r->error = "unexpected object end"; return false; } + if (val->type == SJ_ERROR) { return false; } + return true; +} + + +void sj_location(sj_Reader *r, int *line, int *col) { + int ln = 1, cl = 1; + for (char *p = r->data; p != r->cur; p++) { + if (*p == '\n') { ln++; cl = 0; } + cl++; + } + *line = ln; + *col = cl; +} + +#endif // #ifdef SJ_IMPL \ No newline at end of file diff --git a/gpu-screen-recorder.1 b/gpu-screen-recorder.1 index edb855a..036197b 100644 --- a/gpu-screen-recorder.1 +++ b/gpu-screen-recorder.1 @@ -392,6 +392,14 @@ monotonic_microsec realtime_microsec .fi (default: no). Ignored for live streaming and when output is piped. +.TP +.BI \-ipc " socket_path" +Listen for commands on a unix domain socket at +.I socket_path +instead of only accepting signals. The socket is created when GPU Screen Recorder starts and removed when it exits. +See the +.B IPC +section for the protocol. Has no effect when taking a screenshot. .SS Output Options .TP .BI \-o " output" @@ -461,6 +469,74 @@ Use .B pkill to send signals (e.g., .BR "pkill -SIGUSR1 -f ""^gpu-screen-recorder""" ). +.SH IPC +When the +.B \-ipc +option is used GPU Screen Recorder also listens for commands on a unix domain socket. This gives the same control as signals, +except that a replay can be saved with an arbitrary number of seconds. The socket can only be used by the user that +started GPU Screen Recorder and it's removed when GPU Screen Recorder exits. +.PP +Requests and replies are json objects terminated by a newline. Every request is replied to. +A request has these fields: +.TP +.B id +Number that identifies the request. The reply to the request has the same id. +.TP +.B name +String with the name of the request. +.TP +.B data +Optional, the type depends on the request. +.PP +A reply has these fields: +.TP +.B id +The id of the request that this is a reply to, or 0 when the request had no valid id. +.TP +.B result +Either +.B ok +or +.BR error . +.TP +.B data +Optional. For an +.B error +result this is a string that describes what went wrong. +.PP +These requests are available: +.TP +.B stop +Stop and save recording (stop without save in replay mode). +.TP +.B save-replay +Save replay (replay mode only). +.B data +is the number of seconds to save, which has to be larger than 0. The whole replay buffer is saved when +.B data +is omitted or null. +.TP +.B toggle-pause +Pause/unpause recording (not for streaming/replay). +.TP +.B toggle-replay-recording +Start/stop regular recording during replay/streaming, which requires the +.B \-ro +option. +.PP +Example: +.nf +.RS +gpu-screen-recorder -w screen -f 60 -c mp4 -r 60 -o ~/Videos -ipc "$XDG_RUNTIME_DIR/gsr.sock" & +echo '{"id":1,"name":"save-replay","data":30}' | socat - "UNIX-CONNECT:$XDG_RUNTIME_DIR/gsr.sock" +.RE +.fi +which replies with: +.nf +.RS +{"id":1,"result":"ok"} +.RE +.fi .SH EXAMPLES .B Record monitor at 60 FPS with desktop audio: .nf diff --git a/include/args_parser.h b/include/args_parser.h index 813d4c6..2954dde 100644 --- a/include/args_parser.h +++ b/include/args_parser.h @@ -9,7 +9,7 @@ typedef struct gsr_egl gsr_egl; -#define NUM_ARGS 38 +#define NUM_ARGS 39 typedef enum { GSR_CAPTURE_SOURCE_TYPE_WINDOW, diff --git a/include/cli/ipc.h b/include/cli/ipc.h new file mode 100644 index 0000000..0862c71 --- /dev/null +++ b/include/cli/ipc.h @@ -0,0 +1,56 @@ +#ifndef GSR_CLI_IPC_H +#define GSR_CLI_IPC_H + +#include +#include +#include +#include + +#define GSR_IPC_MAX_CLIENTS 8 +#define GSR_IPC_MAX_REQUEST_SIZE 4096 +#define GSR_IPC_MAX_ERROR_MESSAGE_SIZE 256 + +/* + These are called from the ipc thread while the recording is running. + Return false and write a message to |error_message| to reply to the request with an error. +*/ +typedef struct { + bool (*stop)(char *error_message, size_t error_message_size, void *userdata); + bool (*toggle_pause)(char *error_message, size_t error_message_size, void *userdata); + bool (*toggle_replay_recording)(char *error_message, size_t error_message_size, void *userdata); + /* |seconds| is GSR_SAVE_REPLAY_SECONDS_FULL when the whole replay buffer should be saved */ + bool (*save_replay)(int seconds, char *error_message, size_t error_message_size, void *userdata); + void *userdata; +} gsr_ipc_handlers; + +typedef struct { + int fd; + char request[GSR_IPC_MAX_REQUEST_SIZE]; + size_t request_size; + bool request_too_large; +} gsr_ipc_client; + +/* Receives newline terminated json requests on a unix domain socket and replies to every request */ +typedef struct { + bool initialized; + int socket_fd; + bool socket_bound; + char socket_filepath[PATH_MAX]; + int wakeup_pipe[2]; + gsr_ipc_client clients[GSR_IPC_MAX_CLIENTS]; + int num_clients; + gsr_ipc_handlers handlers; + pthread_t thread; + bool thread_running; +} gsr_ipc; + +/* Returns a |gsr_error| value. Creates the socket, requests are not handled until gsr_ipc_start is called */ +int gsr_ipc_init(gsr_ipc *self, const char *socket_filepath); +void gsr_ipc_deinit(gsr_ipc *self); + +/* Returns a |gsr_error| value. Starts handling requests. Does nothing if |self| hasn't been initialized */ +int gsr_ipc_start(gsr_ipc *self, const gsr_ipc_handlers *handlers); +/* Stops handling requests. Does nothing if the ipc hasn't been started */ +void gsr_ipc_stop(gsr_ipc *self); + +#endif /* GSR_CLI_IPC_H */ diff --git a/include/recorder/audio_capture.h b/include/recorder/audio_capture.h index 68390aa..4f757c9 100644 --- a/include/recorder/audio_capture.h +++ b/include/recorder/audio_capture.h @@ -3,7 +3,7 @@ #include #include -#include +#include #include #include "../sound.h" #include "recording_clock.h" @@ -64,11 +64,11 @@ struct gsr_audio_capture { gsr_encoder *encoder; gsr_recording_clock *clock; - const volatile sig_atomic_t *running; + const atomic_int *running; }; /* Returns a |gsr_error| value */ -int gsr_audio_capture_init(gsr_audio_capture *self, gsr_encoder *encoder, gsr_recording_clock *clock, const volatile sig_atomic_t *running); +int gsr_audio_capture_init(gsr_audio_capture *self, gsr_encoder *encoder, gsr_recording_clock *clock, const atomic_int *running); void gsr_audio_capture_deinit(gsr_audio_capture *self); bool gsr_audio_capture_add_track(gsr_audio_capture *self, const gsr_audio_track *track); diff --git a/include/recorder/replay_save.h b/include/recorder/replay_save.h index d92a8c0..7f48851 100644 --- a/include/recorder/replay_save.h +++ b/include/recorder/replay_save.h @@ -5,7 +5,7 @@ #include #include #include -#include +#include #include "muxer.h" #include "audio_capture.h" #include "capture_setup.h" @@ -25,7 +25,7 @@ typedef struct { typedef struct { pthread_t thread; bool thread_created; - volatile sig_atomic_t finished; + atomic_int finished; bool success; char output_filepath[PATH_MAX]; diff --git a/include/recorder/screenshot.h b/include/recorder/screenshot.h index 057383c..00a7889 100644 --- a/include/recorder/screenshot.h +++ b/include/recorder/screenshot.h @@ -2,7 +2,7 @@ #define GSR_RECORDER_SCREENSHOT_H #include -#include +#include #include "../egl.h" #include "../image_writer.h" #include "../plugins.h" @@ -19,7 +19,7 @@ typedef struct { gsr_image_format image_format; const char **plugin_filepaths; int num_plugin_filepaths; - const volatile sig_atomic_t *running; + const atomic_int *running; void (*screenshot_saved)(const char *filepath, void *userdata); void *userdata; } gsr_screenshot_params; diff --git a/meson.build b/meson.build index 0653470..bddfc02 100644 --- a/meson.build +++ b/meson.build @@ -46,6 +46,7 @@ src = [ 'src/recorder/screenshot.c', 'src/recorder/recorder.c', 'src/cli/commands.c', + 'src/cli/ipc.c', 'src/egl.c', 'src/cuda.c', 'src/window_texture.c', diff --git a/src/args_parser.c b/src/args_parser.c index 0adb5ef..d10da67 100644 --- a/src/args_parser.c +++ b/src/args_parser.c @@ -206,7 +206,7 @@ static void usage_header(void) { "[-cursor yes|no] [-keyint ] [-restore-portal-session yes|no] [-portal-session-token-filepath filepath] [-encoder gpu|cpu] " "[-fallback-cpu-encoding yes|no] [-o ] [-ro ] [-ffmpeg-opts ] [--list-capture-options [card_path]] " "[--list-monitors] [--list-audio-devices] [--list-application-audio] [--list-v4l2-devices] [-write-first-frame-ts yes|no] [-low-power yes|no] " - "[-v yes|no] [-gl-debug yes|no] [-exclude-metadata yes|no] [--version] [-h|--help]\n", program_name); + "[-ipc ] [-v yes|no] [-gl-debug yes|no] [-exclude-metadata yes|no] [--version] [-h|--help]\n", program_name); fflush(stdout); } @@ -571,6 +571,7 @@ args_parse_result args_parser_parse(args_parser *self, int argc, char **argv, co self->args[arg_index++] = (Arg){ .key = "-write-first-frame-ts", .optional = true, .list = false, .type = ARG_TYPE_BOOLEAN }; self->args[arg_index++] = (Arg){ .key = "-low-power", .optional = true, .list = false, .type = ARG_TYPE_BOOLEAN }; self->args[arg_index++] = (Arg){ .key = "-exclude-metadata", .optional = true, .list = false, .type = ARG_TYPE_BOOLEAN }; + self->args[arg_index++] = (Arg){ .key = "-ipc", .optional = true, .list = false, .type = ARG_TYPE_STRING }; assert(arg_index == NUM_ARGS); for(int i = 1; i < argc; i += 2) { diff --git a/src/cli/ipc.c b/src/cli/ipc.c new file mode 100644 index 0000000..6d555c0 --- /dev/null +++ b/src/cli/ipc.c @@ -0,0 +1,555 @@ +#include "../../include/cli/ipc.h" +#include "../../include/recorder/error.h" +#include "../../include/recorder/replay_save.h" +#include "../../include/log.h" + +#define SJ_IMPL +#include "../../external/sj.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define GSR_IPC_MAX_REQUEST_NAME_SIZE 64 +#define GSR_IPC_MAX_ESCAPED_ERROR_MESSAGE_SIZE (GSR_IPC_MAX_ERROR_MESSAGE_SIZE*6) +#define GSR_IPC_MAX_REPLY_SIZE (GSR_IPC_MAX_ESCAPED_ERROR_MESSAGE_SIZE + 128) +#define GSR_IPC_SEND_TIMEOUT_MILLISECONDS 1000 +#define GSR_IPC_SOCKET_MODE 0600 + +typedef struct { + int64_t id; + char name[GSR_IPC_MAX_REQUEST_NAME_SIZE]; + sj_Value data; + bool has_data; +} gsr_ipc_request; + +static void json_escape_string(char *buffer, size_t buffer_size, const char *str) { + char escape_buffer[8]; + size_t offset = 0; + buffer[0] = '\0'; + + for(size_t i = 0; str[i] != '\0'; ++i) { + const unsigned char c = str[i]; + const char *escaped = escape_buffer; + switch(c) { + case '"': escaped = "\\\""; break; + case '\\': escaped = "\\\\"; break; + case '\n': escaped = "\\n"; break; + case '\r': escaped = "\\r"; break; + case '\t': escaped = "\\t"; break; + default: { + if(c < 0x20) + snprintf(escape_buffer, sizeof(escape_buffer), "\\u%04x", c); + else + snprintf(escape_buffer, sizeof(escape_buffer), "%c", c); + break; + } + } + + const size_t escaped_size = strlen(escaped); + if(offset + escaped_size >= buffer_size) + break; + + memcpy(buffer + offset, escaped, escaped_size); + offset += escaped_size; + buffer[offset] = '\0'; + } +} + +static bool json_string_equals(const sj_Value *value, const char *str) { + const size_t value_size = value->end - value->start; + return strlen(str) == value_size && memcmp(value->start, str, value_size) == 0; +} + +static bool json_number_to_int64(const sj_Value *value, int64_t *result) { + char buffer[32]; + const size_t value_size = value->end - value->start; + if(value_size == 0 || value_size >= sizeof(buffer)) + return false; + + memcpy(buffer, value->start, value_size); + buffer[value_size] = '\0'; + + char *number_end = NULL; + errno = 0; + const long long parsed_value = strtoll(buffer, &number_end, 10); + if(errno != 0 || number_end != buffer + value_size) + return false; + + *result = parsed_value; + return true; +} + +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 ipc_send_all(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_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_reply(gsr_ipc_client *client, int64_t id, bool success, const char *error_message) { + char reply[GSR_IPC_MAX_REPLY_SIZE]; + int reply_size = 0; + + 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]; + 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_send_all(client->fd, 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)); + + 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(json_string_equals(&key, "id")) { + id_value = value; + has_id = true; + } else if(json_string_equals(&key, "name")) { + name_value = value; + has_name = true; + } else if(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(id_value.type != SJ_NUMBER || !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 ipc_request_get_save_replay_seconds(const gsr_ipc_request *request, int *seconds, char *error_message, size_t error_message_size) { + *seconds = GSR_SAVE_REPLAY_SECONDS_FULL; + if(!request->has_data || request->data.type == SJ_NULL) + return true; + + int64_t data_seconds = 0; + if(request->data.type != SJ_NUMBER || !json_number_to_int64(&request->data, &data_seconds) || data_seconds <= 0 || data_seconds > INT_MAX) { + snprintf(error_message, error_message_size, "expected 'data' to be the number of seconds to save, which has to be larger than 0"); + return false; + } + + *seconds = data_seconds; + return true; +} + +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, "toggle-replay-recording") == 0) + return self->handlers.toggle_replay_recording(error_message, error_message_size, self->handlers.userdata); + + if(strcmp(request->name, "save-replay") == 0) { + int seconds = GSR_SAVE_REPLAY_SECONDS_FULL; + if(!ipc_request_get_save_replay_seconds(request, &seconds, error_message, error_message_size)) + return false; + + return self->handlers.save_replay(seconds, 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(client, 0, false, "the request is too large"); + + if(string_is_only_whitespace(client->request, client->request_size)) + return ipc_client_send_reply(client, 0, false, "the request is empty"); + + 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(client, request.id, false, error_message); + + if(!ipc_handle_request(self, &request, error_message, sizeof(error_message))) + return ipc_client_send_reply(client, request.id, false, error_message); + + return ipc_client_send_reply(client, request.id, true, 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 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; +} + +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)) { + 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; + ++self->num_clients; +} + +static void ipc_accept_client(gsr_ipc *self) { + const int client_fd = accept(self->socket_fd, NULL, NULL); + if(client_fd == -1) + return; + + ipc_add_client(self, client_fd); +} + +static void ipc_remove_client(gsr_ipc *self, int index) { + close(self->clients[index].fd); + for(int i = index; i < self->num_clients - 1; ++i) { + self->clients[i] = self->clients[i + 1]; + } + --self->num_clients; +} + +static void* ipc_thread(void *userdata) { + gsr_ipc *self = userdata; + struct pollfd poll_fds[2 + GSR_IPC_MAX_CLIENTS]; + + for(;;) { + poll_fds[0].fd = self->wakeup_pipe[0]; + poll_fds[0].events = POLLIN; + poll_fds[0].revents = 0; + poll_fds[1].fd = self->socket_fd; + poll_fds[1].events = POLLIN; + poll_fds[1].revents = 0; + + const int num_polled_clients = self->num_clients; + for(int i = 0; i < num_polled_clients; ++i) { + poll_fds[2 + i].fd = self->clients[i].fd; + poll_fds[2 + i].events = POLLIN; + poll_fds[2 + i].revents = 0; + } + + if(poll(poll_fds, 2 + num_polled_clients, -1) == -1) { + if(errno == EINTR) + continue; + + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_ipc: failed to poll the ipc sockets, error: %s", strerror(errno)); + break; + } + + if(poll_fds[0].revents != 0) + break; + + if(poll_fds[1].revents & POLLIN) + ipc_accept_client(self); + + for(int i = num_polled_clients - 1; i >= 0; --i) { + bool keep_client = true; + if(poll_fds[2 + i].revents & POLLIN) + keep_client = ipc_client_receive(self, &self->clients[i]); + else if(poll_fds[2 + i].revents & (POLLHUP | POLLERR | POLLNVAL)) + keep_client = false; + + if(!keep_client) + ipc_remove_client(self, i); + } + } + + 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; +} + +static bool ipc_bind(gsr_ipc *self, const struct sockaddr_un *addr) { + const mode_t prev_mask = umask(0777 & ~GSR_IPC_SOCKET_MODE); + int bind_result = bind(self->socket_fd, (const struct sockaddr*)addr, sizeof(*addr)); + if(bind_result == -1 && errno == EADDRINUSE && !ipc_socket_filepath_in_use(self->socket_filepath)) { + unlink(self->socket_filepath); + 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->socket_fd != -1) { + close(self->socket_fd); + self->socket_fd = -1; + } + + if(self->socket_bound) { + unlink(self->socket_filepath); + self->socket_bound = false; + } +} + +int gsr_ipc_init(gsr_ipc *self, const char *socket_filepath) { + memset(self, 0, sizeof(*self)); + self->socket_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(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])) { + 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_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; + + const char wakeup_value = 1; + 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_stop: failed to wake up the ipc thread, error: %s", strerror(errno)); + + pthread_join(self->thread, NULL); + self->thread_running = false; +} diff --git a/src/cli/main.c b/src/cli/main.c index 9d5f7e2..5ead4a4 100644 --- a/src/cli/main.c +++ b/src/cli/main.c @@ -16,6 +16,7 @@ */ #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" @@ -39,10 +40,11 @@ #include #include #include +#include #include #include -static volatile sig_atomic_t running = 1; +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; @@ -51,7 +53,7 @@ static volatile sig_atomic_t pending_save_replay_seconds = 0; static void stop_handler(int signal_value) { (void)signal_value; - running = 0; + atomic_store(&running, 0); if(recorder) gsr_recorder_stop(recorder); } @@ -132,6 +134,48 @@ static void save_replay_30_minutes_handler(int 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_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_save_replay_handler(int seconds, 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; + } + + gsr_recorder_save_replay(recorder, seconds); + return true; +} + static void install_signal_handlers(void) { signal(SIGINT, stop_handler); signal(SIGTERM, stop_handler); @@ -366,7 +410,7 @@ static int take_screenshot(args_parser *arg_parser, gsr_windowing *windowing, gs 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) { +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); @@ -396,10 +440,21 @@ static int record(args_parser *arg_parser, gsr_windowing *windowing, gsr_capture return error; apply_pending_signals(recorder); - if(!running) + if(!atomic_load(&running)) gsr_recorder_stop(recorder); - const int run_result = gsr_recorder_run(recorder); + gsr_ipc_handlers ipc_handlers; + ipc_handlers.stop = ipc_stop_handler; + ipc_handlers.toggle_pause = ipc_toggle_pause_handler; + ipc_handlers.toggle_replay_recording = ipc_toggle_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_stop(ipc); gsr_recorder_destroy(recorder); recorder = NULL; return run_result; @@ -413,11 +468,16 @@ static int run(args_parser *arg_parser) { 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); @@ -437,6 +497,11 @@ static int run(args_parser *arg_parser) { 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); @@ -518,12 +583,16 @@ static int run(args_parser *arg_parser) { 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)); + 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 diff --git a/src/recorder/audio_capture.c b/src/recorder/audio_capture.c index ee1c75f..6275c93 100644 --- a/src/recorder/audio_capture.c +++ b/src/recorder/audio_capture.c @@ -155,7 +155,7 @@ static void* audio_device_thread(void *userdata) { gsr_audio_track *track = thread_userdata->track; gsr_audio_device_capture *device = thread_userdata->device; gsr_recording_clock *clock = self->clock; - const volatile sig_atomic_t *running = self->running; + const atomic_int *running = self->running; const enum AVSampleFormat sound_device_sample_format = audio_format_to_sample_format(audio_codec_context_get_audio_format(track->codec_context)); /* TODO: Always do conversion for now. This fixes issue with stuttering audio on pulseaudio with opus + multiple audio sources merged */ @@ -194,7 +194,7 @@ static void* audio_device_thread(void *userdata) { if(device->sound_device.handle) sound_device_flush(&device->sound_device); - while(*running) { + while(atomic_load(running)) { void *sound_buffer; int sound_buffer_size = -1; const double time_before_read_seconds = clock_get_monotonic_seconds(); @@ -332,7 +332,7 @@ static void* audio_device_thread(void *userdata) { static void* amix_thread(void *userdata) { gsr_audio_capture *self = userdata; AVFrame *aframe = av_frame_alloc(); - while(*self->running) { + while(atomic_load(self->running)) { pthread_mutex_lock(&self->filter_mutex); for(size_t i = 0; i < self->num_tracks; ++i) { gsr_audio_track *track = &self->tracks[i]; @@ -360,7 +360,7 @@ static void* amix_thread(void *userdata) { return NULL; } -int gsr_audio_capture_init(gsr_audio_capture *self, gsr_encoder *encoder, gsr_recording_clock *clock, const volatile sig_atomic_t *running) { +int gsr_audio_capture_init(gsr_audio_capture *self, gsr_encoder *encoder, gsr_recording_clock *clock, const atomic_int *running) { memset(self, 0, sizeof(*self)); self->encoder = encoder; self->clock = clock; diff --git a/src/recorder/recorder.c b/src/recorder/recorder.c index 7c45594..6f9ea12 100644 --- a/src/recorder/recorder.c +++ b/src/recorder/recorder.c @@ -26,6 +26,7 @@ #include #include #include +#include #include #include @@ -85,10 +86,10 @@ struct gsr_recorder { int64_t video_prev_pts; bool hdr_metadata_set; - volatile sig_atomic_t running; - volatile sig_atomic_t toggle_pause; - volatile sig_atomic_t toggle_replay_recording; - volatile sig_atomic_t save_replay_seconds; + atomic_int running; + atomic_int toggle_pause; + atomic_int toggle_replay_recording; + atomic_int save_replay_seconds; bool should_stop_error; bool force_iframe_frame; int audio_max_frame_size; @@ -400,7 +401,10 @@ gsr_recorder* gsr_recorder_create(const gsr_recorder_params *params, const gsr_r self->capture_deps = params->capture_deps; self->capture_sources = params->capture_sources; self->audio_input_tracks = params->audio_input_tracks; - self->running = 1; + atomic_init(&self->running, 1); + atomic_init(&self->toggle_pause, 0); + atomic_init(&self->toggle_replay_recording, 0); + atomic_init(&self->save_replay_seconds, 0); self->audio_max_frame_size = 1024; int error_code = GSR_ERROR_GENERIC; self->hdr = video_codec_is_hdr(params->settings->video_codec); @@ -465,7 +469,7 @@ static bool recorder_tick_video_sources(gsr_recorder *self) { gsr_capture_tick(video_source->capture); if(gsr_capture_should_stop(video_source->capture, &self->should_stop_error)) { - self->running = 0; + atomic_store(&self->running, 0); break; } @@ -621,23 +625,23 @@ static void recorder_capture_and_encode_frame(gsr_recorder *self, bool damaged) } static void recorder_apply_pause_toggle(gsr_recorder *self) { - if(self->toggle_pause == 1 && !self->settings.is_replaying) { + if(atomic_load(&self->toggle_pause) == 1 && !self->settings.is_replaying) { self->paused = !self->paused; gsr_recording_clock_set_paused(self->recording_clock, self->paused); gsr_log(GSR_LOG_LEVEL_INFO, self->paused ? "Paused" : "Unpaused"); - self->toggle_pause = 0; + atomic_store(&self->toggle_pause, 0); } } static void recorder_apply_replay_recording_toggle(gsr_recorder *self) { - if(self->toggle_replay_recording && !self->settings.replay_recording_directory) { - self->toggle_replay_recording = 0; + if(atomic_load(&self->toggle_replay_recording) && !self->settings.replay_recording_directory) { + atomic_store(&self->toggle_replay_recording, 0); if(self->callbacks.recording_started) self->callbacks.recording_started(NULL, self->callbacks.userdata); } - if(self->toggle_replay_recording && self->settings.replay_recording_directory) { - self->toggle_replay_recording = 0; + if(atomic_load(&self->toggle_replay_recording) && self->settings.replay_recording_directory) { + atomic_store(&self->toggle_replay_recording, 0); const bool new_replay_recording_state = !self->replay_recording; if(new_replay_recording_state) { gsr_audio_capture_lock_filter(&self->audio_capture); @@ -704,12 +708,12 @@ static void recorder_poll_replay_save(gsr_recorder *self) { self->callbacks.replay_saved(replay_save_output_filepath[0] == '\0' || !replay_save_result ? NULL : replay_save_output_filepath, self->callbacks.userdata); } - if(self->save_replay_seconds != 0 && !gsr_replay_save_is_running(&self->replay_save) && self->settings.is_replaying) { - int current_save_replay_seconds = self->save_replay_seconds; + if(atomic_load(&self->save_replay_seconds) != 0 && !gsr_replay_save_is_running(&self->replay_save) && self->settings.is_replaying) { + int current_save_replay_seconds = atomic_load(&self->save_replay_seconds); if(current_save_replay_seconds > 0) current_save_replay_seconds += self->settings.keyint; - self->save_replay_seconds = 0; + atomic_store(&self->save_replay_seconds, 0); const bool replay_start_result = gsr_replay_save_start(&self->replay_save, self->video_codec_context, GSR_VIDEO_STREAM_INDEX, &self->audio_capture, &self->encoder, &self->settings, self->file_extension, self->hdr, self->video_sources, current_save_replay_seconds); if(!replay_start_result && self->callbacks.replay_saved) self->callbacks.replay_saved(NULL, self->callbacks.userdata); @@ -762,7 +766,7 @@ int gsr_recorder_run(gsr_recorder *self) { if(gsr_audio_capture_start(&self->audio_capture, self->audio_max_frame_size, self->uses_amix) != GSR_ERROR_OK) { /* The audio threads that did start have to stop before they can be joined */ - self->running = 0; + atomic_store(&self->running, 0); return GSR_ERROR_GENERIC; } @@ -802,7 +806,7 @@ int gsr_recorder_run(gsr_recorder *self) { } } - while(self->running) { + while(atomic_load(&self->running)) { recorder_process_events(self); const bool damaged = recorder_tick_video_sources(self); recorder_update_fps_counters(self); @@ -818,7 +822,7 @@ int gsr_recorder_run(gsr_recorder *self) { } static void gsr_recorder_stop_recording(gsr_recorder *self) { - self->running = 0; + atomic_store(&self->running, 0); bool final_replay_save_result = false; const char *final_replay_save_output_filepath = NULL; @@ -894,17 +898,17 @@ void gsr_recorder_destroy(gsr_recorder *self) { } void gsr_recorder_stop(gsr_recorder *self) { - self->running = 0; + atomic_store(&self->running, 0); } void gsr_recorder_toggle_pause(gsr_recorder *self) { - self->toggle_pause = 1; + atomic_store(&self->toggle_pause, 1); } void gsr_recorder_toggle_replay_recording(gsr_recorder *self) { - self->toggle_replay_recording = 1; + atomic_store(&self->toggle_replay_recording, 1); } void gsr_recorder_save_replay(gsr_recorder *self, int seconds) { - self->save_replay_seconds = seconds; + atomic_store(&self->save_replay_seconds, seconds); } diff --git a/src/recorder/replay_save.c b/src/recorder/replay_save.c index 6e5bc7b..1d33437 100644 --- a/src/recorder/replay_save.c +++ b/src/recorder/replay_save.c @@ -8,7 +8,7 @@ void gsr_replay_save_init(gsr_replay_save *self) { memset(self, 0, sizeof(*self)); - self->finished = 0; + atomic_init(&self->finished, 0); } bool gsr_replay_save_is_running(const gsr_replay_save *self) { @@ -110,7 +110,7 @@ static void* replay_save_thread(void *userdata) { self->success = success; gsr_replay_save_cleanup(self); - self->finished = 1; + atomic_store(&self->finished, 1); return NULL; } @@ -123,7 +123,7 @@ bool gsr_replay_save_start(gsr_replay_save *self, AVCodecContext *video_codec_co self->video_stream_index = video_stream_index; self->output_filepath[0] = '\0'; self->success = false; - self->finished = 0; + atomic_store(&self->finished, 0); pthread_mutex_lock(&encoder->replay_mutex); self->cloned_replay_buffer = gsr_replay_buffer_clone(encoder->replay_buffer); @@ -193,7 +193,7 @@ static bool gsr_replay_save_finish(gsr_replay_save *self, bool *success, const c } bool gsr_replay_save_poll(gsr_replay_save *self, bool *success, const char **output_filepath) { - if(!self->thread_created || !self->finished) + if(!self->thread_created || !atomic_load(&self->finished)) return false; return gsr_replay_save_finish(self, success, output_filepath); diff --git a/src/recorder/screenshot.c b/src/recorder/screenshot.c index 47fedef..2e4357d 100644 --- a/src/recorder/screenshot.c +++ b/src/recorder/screenshot.c @@ -67,7 +67,7 @@ int gsr_screenshot_take(const gsr_screenshot_params *params) { gsr_window *window = params->window; gsr_capture_deps *capture_deps = params->capture_deps; gsr_capture_sources *capture_sources = params->capture_sources; - const volatile sig_atomic_t *running = params->running; + const atomic_int *running = params->running; const int image_quality = video_quality_to_image_quality_value(settings->video_quality); const gsr_color_range color_range = image_format_to_color_range(params->image_format, image_quality); @@ -122,7 +122,7 @@ int gsr_screenshot_take(const gsr_screenshot_params *params) { bool should_stop_error = false; egl->glClear(0); - while(*running) { + while(atomic_load(running)) { while(gsr_window_process_event(window)) { if(capture_deps->x11_cursor_display && settings->record_cursor) gsr_cursor_on_event(&capture_deps->x11_cursor, gsr_window_get_event_data(window)); @@ -175,7 +175,7 @@ int gsr_screenshot_take(const gsr_screenshot_params *params) { if(all_sources_captured) break; - if(*running) + if(atomic_load(running)) usleep(30 * 1000); // 30 ms } -- cgit v1.2.3 From 9b799f6e45c5e7a44432824f69d9632ff5848e6c Mon Sep 17 00:00:00 2001 From: dec05eba Date: Sun, 2 Aug 2026 01:35:27 +0200 Subject: Only remove the ipc socket path when an unused socket exists there The socket that a killed GPU Screen Recorder instance leaves behind is removed so that the same ipc socket path can be used again, but this was done for any type of file, so using the path of a regular file as the ipc socket path deleted that file. --- src/cli/ipc.c | 29 ++++++++++++++++++++++++----- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/src/cli/ipc.c b/src/cli/ipc.c index 6d555c0..5a43283 100644 --- a/src/cli/ipc.c +++ b/src/cli/ipc.c @@ -412,13 +412,32 @@ static bool ipc_socket_filepath_in_use(const char *socket_filepath) { 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); - int bind_result = bind(self->socket_fd, (const struct sockaddr*)addr, sizeof(*addr)); - if(bind_result == -1 && errno == EADDRINUSE && !ipc_socket_filepath_in_use(self->socket_filepath)) { - unlink(self->socket_filepath); - bind_result = bind(self->socket_fd, (const struct sockaddr*)addr, sizeof(*addr)); - } + const int bind_result = bind(self->socket_fd, (const struct sockaddr*)addr, sizeof(*addr)); const int bind_error = errno; umask(prev_mask); -- cgit v1.2.3 From f547fcc354671c672fffb20092e72d231e6ed1e4 Mon Sep 17 00:00:00 2001 From: dec05eba Date: Sun, 2 Aug 2026 01:36:33 +0200 Subject: Add gsr-cli program that sends ipc commands to gpu-screen-recorder gsr-cli sends a command to the ipc socket of a running GPU Screen Recorder and exits with 0 when it replied with ok, printing the reason to stderr when it didn't, which means the commands don't have to be sent blindly like signals are. The status command checks if GPU Screen Recorder is running by connecting to the socket without sending a command, so it's also correct when a killed instance left the socket behind. The json helpers moved into src/json.c to be shared between the two programs. --- README.md | 5 +- gpu-screen-recorder.1 | 7 ++ gsr-cli.1 | 100 +++++++++++++++++ include/json.h | 15 +++ meson.build | 4 +- project.conf | 2 +- src/cli/ipc.c | 74 ++----------- src/gsr_cli/main.c | 298 ++++++++++++++++++++++++++++++++++++++++++++++++++ src/json.c | 69 ++++++++++++ 9 files changed, 503 insertions(+), 71 deletions(-) create mode 100644 gsr-cli.1 create mode 100644 include/json.h create mode 100644 src/gsr_cli/main.c create mode 100644 src/json.c diff --git a/README.md b/README.md index 089076e..1b7502d 100644 --- a/README.md +++ b/README.md @@ -140,8 +140,9 @@ To stop recording send SIGINT to gpu screen recorder. You can do this by running To pause/unpause recording send SIGUSR2 to gpu screen recorder. You can do this by running `pkill -SIGUSR2 -f "^gpu-screen-recorder"`. This is only applicable and useful when recording (not streaming nor replay).\ There are more signals to control GPU Screen Recorder. Run `gpu-screen-recorder --help` to list them all (under `NOTES` section).\ GPU Screen Recorder can also be controlled with json messages over a unix domain socket by launching it with the `-ipc` option, for example `-ipc "$XDG_RUNTIME_DIR/gsr.sock"`. -This gives the same control as the signals, except that a replay can be saved with an arbitrary number of seconds, for example `echo '{"id":1,"name":"save-replay","data":30}' | socat - "UNIX-CONNECT:$XDG_RUNTIME_DIR/gsr.sock"`. -See the `IPC` section in the man page (`man gpu-screen-recorder`) for the full protocol. +This gives the same control as the signals, except that a replay can be saved with an arbitrary number of seconds and that you get a reply that says if the command succeeded.\ +The `gsr-cli` program sends these commands, for example `gsr-cli -ipc "$XDG_RUNTIME_DIR/gsr.sock" save-replay 30`. It can also check if GPU Screen Recorder is running with `gsr-cli -ipc "$XDG_RUNTIME_DIR/gsr.sock" status`.\ +See `man gsr-cli` for all commands and the `IPC` section in `man gpu-screen-recorder` for the protocol, if you want to talk to the socket directly instead of using `gsr-cli`. ## Simple way to run replay without gui Run the script `scripts/start-replay.sh` to start replay and then `scripts/save-replay.sh` to save a replay and `scripts/stop-replay.sh` to stop the replay. The videos are saved to `$HOME/Videos`. You can use these scripts to start replay at system startup if you add `scripts/start-replay.sh` to startup (this can be done differently depending on your desktop environment / window manager) and then go into diff --git a/gpu-screen-recorder.1 b/gpu-screen-recorder.1 index 036197b..af723e8 100644 --- a/gpu-screen-recorder.1 +++ b/gpu-screen-recorder.1 @@ -476,6 +476,10 @@ option is used GPU Screen Recorder also listens for commands on a unix domain so except that a replay can be saved with an arbitrary number of seconds. The socket can only be used by the user that started GPU Screen Recorder and it's removed when GPU Screen Recorder exits. .PP +.BR gsr\-cli (1) +is a program that sends these commands and reports the reply, so the protocol below only has to be +implemented by programs that want to talk to the socket directly. +.PP Requests and replies are json objects terminated by a newline. Every request is replied to. A request has these fields: .TP @@ -687,6 +691,9 @@ The bug may have been previously reported or may not be related to gpu-screen-r .SH COPYRIGHT Copyright © dec05eba. Licensed under GPL-3.0-only. .SH SEE ALSO +.BR gsr\-cli (1), +.BR gsr\-kms\-server (1) +.PP .UR https://git.dec05eba.com/gpu-screen-recorder Project homepage .UE diff --git a/gsr-cli.1 b/gsr-cli.1 new file mode 100644 index 0000000..0c6084e --- /dev/null +++ b/gsr-cli.1 @@ -0,0 +1,100 @@ +.TH GSR\-CLI 1 "2026-08-02" "5.15.3" "GPU Screen Recorder CLI Manual" +.SH NAME +gsr\-cli \- Control a running GPU Screen Recorder instance +.SH SYNOPSIS +.B gsr\-cli +.B \-ipc +.I socket_path +.I command +.RI [ command_argument ] +.PP +.B gsr\-cli +.B \-h +| +.B \-\-help +.SH DESCRIPTION +.B gsr\-cli +sends a command to a +.BR gpu\-screen\-recorder (1) +instance that was started with the +.B \-ipc +option and reports the reply. This gives the same control as sending signals to GPU Screen Recorder, +except that a replay can be saved with an arbitrary number of seconds and that the result of the +command is known instead of being sent blindly. +.PP +.B gsr\-cli +exits with 0 when the command succeeded and 1 when it failed. When a command fails the reason +is printed to stderr. +.SH OPTIONS +.TP +.BI \-ipc " socket_path" +The unix domain socket that GPU Screen Recorder was started with. Required. +.SH COMMANDS +.TP +.B status +Check if a GPU Screen Recorder instance is listening on the socket, which is done by connecting +to the socket without sending a command. Prints +.B running +or +.B "not running" +and exits with 0 when it's running. A socket file that was left behind by a GPU Screen Recorder +instance that was killed counts as not running. +.TP +.B stop +Stop and save recording (stop without save in replay mode). +.TP +.B toggle-pause +Pause/unpause recording (not for streaming/replay). +.TP +.B toggle-replay-recording +Start/stop regular recording during replay/streaming, which requires GPU Screen Recorder to run +with the +.B \-ro +option. +.TP +.BR save-replay " [" \fIseconds\fR ] +Save replay (replay mode only). The number of seconds has to be larger than 0. The whole replay +buffer is saved when no number of seconds is given. +.SH EXAMPLES +Start GPU Screen Recorder in replay mode with an ipc socket: +.RS +.nf +gpu-screen-recorder -w screen -f 60 -c mp4 -r 60 -o ~/Videos -ipc "$XDG_RUNTIME_DIR/gsr.sock" & +.fi +.RE +.PP +Save the last 30 seconds: +.RS +.nf +gsr-cli -ipc "$XDG_RUNTIME_DIR/gsr.sock" save-replay 30 +.fi +.RE +.PP +Start replay only when it isn't already running: +.RS +.nf +gsr-cli -ipc "$XDG_RUNTIME_DIR/gsr.sock" status >/dev/null || start-replay.sh +.fi +.RE +.SH NOTES +.IP \(bu 3 +The socket can only be used by the user that started GPU Screen Recorder. +.IP \(bu 3 +A command is replied to as soon as GPU Screen Recorder accepts it. Saving a video finishes +after the reply, and the path to the saved video is printed by GPU Screen Recorder itself. +.IP \(bu 3 +GPU Screen Recorder creates the socket before it has finished starting up, so a command that is +sent while it's starting up is replied to when the recording starts. +.SH AUTHORS +gsr\-cli was written by the GPU Screen Recorder contributors. +.SH REPORTING BUGS +Report bugs to: +.UR mailto:dec05eba@protonmail.com +dec05eba@protonmail.com +.UE . +.SH COPYRIGHT +Copyright © dec05eba. Licensed under GPL-3.0-only. +.SH SEE ALSO +.BR gpu\-screen\-recorder (1) +.PP +Project homepage: diff --git a/include/json.h b/include/json.h new file mode 100644 index 0000000..3bf8a9e --- /dev/null +++ b/include/json.h @@ -0,0 +1,15 @@ +#ifndef GSR_JSON_H +#define GSR_JSON_H + +#include +#include +#include +#include "../external/sj.h" + +bool gsr_json_string_equals(const sj_Value *value, const char *str); +/* Fails if |value| is not a json number without a fractional part */ +bool gsr_json_number_to_int64(const sj_Value *value, int64_t *result); +/* An escaped string can become 6 times as large as |str|. The result is truncated when it doesn't fit in |buffer| */ +void gsr_json_escape_string(char *buffer, size_t buffer_size, const char *str); + +#endif /* GSR_JSON_H */ diff --git a/meson.build b/meson.build index bddfc02..7b0c14e 100644 --- a/meson.build +++ b/meson.build @@ -31,6 +31,7 @@ src = [ 'src/replay_buffer/replay_buffer_ram.c', 'src/replay_buffer/replay_buffer_disk.c', 'src/log.c', + 'src/json.c', 'src/ffmpeg_utils.c', 'src/recorder/audio_codec.c', 'src/recorder/video_codec.c', @@ -131,10 +132,11 @@ endif add_project_arguments('-DGSR_VERSION="' + meson.project_version() + '"', language: 'c') executable('gsr-kms-server', 'kms/server/kms_server.c', dependencies : dependency('libdrm'), c_args : '-fstack-protector-all', install : true) +executable('gsr-cli', ['src/gsr_cli/main.c', 'src/json.c', 'src/log.c'], install : true) executable('gpu-screen-recorder', src, dependencies : dep, install : true) install_headers('plugin/plugin.h', install_dir : 'include/gsr') -install_man('gpu-screen-recorder.1', 'gsr-kms-server.1') +install_man('gpu-screen-recorder.1', 'gsr-cli.1', 'gsr-kms-server.1') install_subdir('scripts', install_dir: 'share/gpu-screen-recorder') if get_option('systemd') == true diff --git a/project.conf b/project.conf index 0e0ed9c..38af0ba 100644 --- a/project.conf +++ b/project.conf @@ -8,7 +8,7 @@ platforms = ["posix"] version = "c++17" [config] -ignore_dirs = ["kms/server", "build", "debug-build", "plugin/examples"] +ignore_dirs = ["kms/server", "src/gsr_cli", "build", "debug-build", "plugin/examples"] #error_on_warning = "true" [define] diff --git a/src/cli/ipc.c b/src/cli/ipc.c index 5a43283..b9d0c51 100644 --- a/src/cli/ipc.c +++ b/src/cli/ipc.c @@ -1,13 +1,10 @@ #include "../../include/cli/ipc.h" #include "../../include/recorder/error.h" #include "../../include/recorder/replay_save.h" +#include "../../include/json.h" #include "../../include/log.h" -#define SJ_IMPL -#include "../../external/sj.h" - #include -#include #include #include #include @@ -33,63 +30,6 @@ typedef struct { bool has_data; } gsr_ipc_request; -static void json_escape_string(char *buffer, size_t buffer_size, const char *str) { - char escape_buffer[8]; - size_t offset = 0; - buffer[0] = '\0'; - - for(size_t i = 0; str[i] != '\0'; ++i) { - const unsigned char c = str[i]; - const char *escaped = escape_buffer; - switch(c) { - case '"': escaped = "\\\""; break; - case '\\': escaped = "\\\\"; break; - case '\n': escaped = "\\n"; break; - case '\r': escaped = "\\r"; break; - case '\t': escaped = "\\t"; break; - default: { - if(c < 0x20) - snprintf(escape_buffer, sizeof(escape_buffer), "\\u%04x", c); - else - snprintf(escape_buffer, sizeof(escape_buffer), "%c", c); - break; - } - } - - const size_t escaped_size = strlen(escaped); - if(offset + escaped_size >= buffer_size) - break; - - memcpy(buffer + offset, escaped, escaped_size); - offset += escaped_size; - buffer[offset] = '\0'; - } -} - -static bool json_string_equals(const sj_Value *value, const char *str) { - const size_t value_size = value->end - value->start; - return strlen(str) == value_size && memcmp(value->start, str, value_size) == 0; -} - -static bool json_number_to_int64(const sj_Value *value, int64_t *result) { - char buffer[32]; - const size_t value_size = value->end - value->start; - if(value_size == 0 || value_size >= sizeof(buffer)) - return false; - - memcpy(buffer, value->start, value_size); - buffer[value_size] = '\0'; - - char *number_end = NULL; - errno = 0; - const long long parsed_value = strtoll(buffer, &number_end, 10); - if(errno != 0 || number_end != buffer + value_size) - return false; - - *result = parsed_value; - return true; -} - 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') @@ -139,7 +79,7 @@ static bool ipc_client_send_reply(gsr_ipc_client *client, int64_t id, bool succe reply_size = snprintf(reply, sizeof(reply), "{\"id\":%" PRIi64 ",\"result\":\"ok\"}\n", id); } else { char escaped_error_message[GSR_IPC_MAX_ESCAPED_ERROR_MESSAGE_SIZE]; - json_escape_string(escaped_error_message, sizeof(escaped_error_message), error_message ? error_message : ""); + 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); } @@ -169,13 +109,13 @@ static bool ipc_request_parse(char *data, size_t size, gsr_ipc_request *request, sj_Value key; sj_Value value; while(sj_iter_object(&reader, root, &key, &value)) { - if(json_string_equals(&key, "id")) { + if(gsr_json_string_equals(&key, "id")) { id_value = value; has_id = true; - } else if(json_string_equals(&key, "name")) { + } else if(gsr_json_string_equals(&key, "name")) { name_value = value; has_name = true; - } else if(json_string_equals(&key, "data")) { + } else if(gsr_json_string_equals(&key, "data")) { request->data = value; request->has_data = true; } @@ -191,7 +131,7 @@ static bool ipc_request_parse(char *data, size_t size, gsr_ipc_request *request, return false; } - if(id_value.type != SJ_NUMBER || !json_number_to_int64(&id_value, &request->id)) { + if(!gsr_json_number_to_int64(&id_value, &request->id)) { snprintf(error_message, error_message_size, "expected 'id' to be an integer"); return false; } @@ -216,7 +156,7 @@ static bool ipc_request_get_save_replay_seconds(const gsr_ipc_request *request, return true; int64_t data_seconds = 0; - if(request->data.type != SJ_NUMBER || !json_number_to_int64(&request->data, &data_seconds) || data_seconds <= 0 || data_seconds > INT_MAX) { + if(!gsr_json_number_to_int64(&request->data, &data_seconds) || data_seconds <= 0 || data_seconds > INT_MAX) { snprintf(error_message, error_message_size, "expected 'data' to be the number of seconds to save, which has to be larger than 0"); return false; } diff --git a/src/gsr_cli/main.c b/src/gsr_cli/main.c new file mode 100644 index 0000000..fb0fcbf --- /dev/null +++ b/src/gsr_cli/main.c @@ -0,0 +1,298 @@ +/* + Copyright (C) 2020 dec05eba + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#include "../../include/json.h" +#include "../../include/log.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define GSR_CLI_REQUEST_ID 1 +#define GSR_CLI_MAX_REQUEST_SIZE 256 +#define GSR_CLI_MAX_REPLY_SIZE 4096 +#define GSR_CLI_REPLY_TIMEOUT_SECONDS 10 + +static void usage(void) { + printf("usage: gsr-cli -ipc [command_argument]\n"); + printf("\n"); + printf("Sends a command to a GPU Screen Recorder instance that was started with the -ipc option.\n"); + printf("\n"); + printf("OPTIONS:\n"); + printf(" -ipc \n"); + printf(" The unix domain socket that GPU Screen Recorder was started with. Required.\n"); + printf("\n"); + printf("COMMANDS:\n"); + printf(" status\n"); + printf(" Check if a GPU Screen Recorder instance is listening on the socket. Prints \"running\" or\n"); + printf(" \"not running\" and exits with 0 when it's running.\n"); + printf(" stop\n"); + printf(" Stop and save the recording (stop without save in replay mode).\n"); + printf(" toggle-pause\n"); + printf(" Pause/unpause the recording (not for streaming/replay).\n"); + printf(" toggle-replay-recording\n"); + printf(" Start/stop a regular recording during replay/streaming.\n"); + printf(" save-replay [seconds]\n"); + printf(" Save the replay. The number of seconds has to be larger than 0. The whole replay buffer is\n"); + printf(" saved when no number of seconds is given.\n"); + printf("\n"); + printf("EXAMPLES:\n"); + printf(" gsr-cli -ipc \"$XDG_RUNTIME_DIR/gsr.sock\" status\n"); + printf(" gsr-cli -ipc \"$XDG_RUNTIME_DIR/gsr.sock\" save-replay 30\n"); + fflush(stdout); +} + +static bool string_to_int64(const char *str, int64_t *result) { + char *number_end = NULL; + errno = 0; + const long long parsed_value = strtoll(str, &number_end, 10); + if(errno != 0 || number_end == str || *number_end != '\0') + return false; + + *result = parsed_value; + return true; +} + +/* Returns the socket, or -1 on failure. Only logs an error when the failure isn't a missing GPU Screen Recorder instance */ +static int ipc_connect(const char *socket_filepath) { + 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, "the ipc socket path is too long, it can be at most %d characters: \"%s\"", (int)sizeof(addr.sun_path) - 1, socket_filepath); + return -1; + } + + const int fd = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0); + if(fd == -1) { + gsr_log(GSR_LOG_LEVEL_ERROR, "failed to create a socket, error: %s", strerror(errno)); + return -1; + } + + struct timeval timeout; + timeout.tv_sec = GSR_CLI_REPLY_TIMEOUT_SECONDS; + timeout.tv_usec = 0; + setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout)); + setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &timeout, sizeof(timeout)); + + if(connect(fd, (const struct sockaddr*)&addr, sizeof(addr)) == -1) { + close(fd); + return -1; + } + + return fd; +} + +static bool ipc_send_all(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; + + gsr_log(GSR_LOG_LEVEL_ERROR, "failed to send the request, error: %s", strerror(errno)); + return false; + } + return true; +} + +/* Reads until a newline. |reply_size| is set to the size of the reply, excluding the newline */ +static bool ipc_receive_reply(int fd, char *reply, size_t reply_capacity, size_t *reply_size) { + size_t offset = 0; + for(;;) { + if(offset == reply_capacity) { + gsr_log(GSR_LOG_LEVEL_ERROR, "the reply is too large"); + return false; + } + + const ssize_t bytes_read = recv(fd, reply + offset, reply_capacity - offset, 0); + if(bytes_read == 0) { + gsr_log(GSR_LOG_LEVEL_ERROR, "GPU Screen Recorder closed the connection before replying"); + return false; + } + + if(bytes_read == -1) { + if(errno == EINTR) + continue; + + if(errno == EAGAIN || errno == EWOULDBLOCK) + gsr_log(GSR_LOG_LEVEL_ERROR, "timed out after %d seconds waiting for a reply", GSR_CLI_REPLY_TIMEOUT_SECONDS); + else + gsr_log(GSR_LOG_LEVEL_ERROR, "failed to receive the reply, error: %s", strerror(errno)); + return false; + } + + const char *newline = memchr(reply + offset, '\n', bytes_read); + offset += bytes_read; + if(newline) { + *reply_size = newline - reply; + return true; + } + } +} + +/* Returns the exit code that gsr-cli should exit with */ +static int ipc_handle_reply(char *reply, size_t reply_size, int64_t request_id) { + sj_Reader reader = sj_reader(reply, reply_size); + const sj_Value root = sj_read(&reader); + if(root.type != SJ_OBJECT) { + gsr_log(GSR_LOG_LEVEL_ERROR, "expected the reply to be a json object, got: %.*s", (int)reply_size, reply); + return 1; + } + + int64_t id = 0; + bool has_id = false; + sj_Value result_value; + bool has_result = false; + sj_Value data_value; + bool has_data = false; + + sj_Value key; + sj_Value value; + while(sj_iter_object(&reader, root, &key, &value)) { + if(gsr_json_string_equals(&key, "id")) { + has_id = gsr_json_number_to_int64(&value, &id); + } else if(gsr_json_string_equals(&key, "result")) { + result_value = value; + has_result = value.type == SJ_STRING; + } else if(gsr_json_string_equals(&key, "data")) { + data_value = value; + has_data = true; + } + } + + if(reader.error) { + gsr_log(GSR_LOG_LEVEL_ERROR, "failed to parse the reply: %s", reader.error); + return 1; + } + + if(!has_id || id != request_id) { + gsr_log(GSR_LOG_LEVEL_ERROR, "received a reply to another request: %.*s", (int)reply_size, reply); + return 1; + } + + if(!has_result) { + gsr_log(GSR_LOG_LEVEL_ERROR, "the reply is missing the 'result' field: %.*s", (int)reply_size, reply); + return 1; + } + + if(gsr_json_string_equals(&result_value, "ok")) + return 0; + + if(has_data && data_value.type == SJ_STRING) + gsr_log(GSR_LOG_LEVEL_ERROR, "%.*s", (int)(data_value.end - data_value.start), data_value.start); + else + gsr_log(GSR_LOG_LEVEL_ERROR, "the request failed: %.*s", (int)reply_size, reply); + + return 1; +} + +static int status_command(const char *socket_filepath) { + const int fd = ipc_connect(socket_filepath); + if(fd == -1) { + printf("not running\n"); + fflush(stdout); + return 1; + } + + close(fd); + printf("running\n"); + fflush(stdout); + return 0; +} + +static int send_command(const char *socket_filepath, const char *name, const char *seconds_str) { + char request[GSR_CLI_MAX_REQUEST_SIZE]; + if(seconds_str) { + int64_t seconds = 0; + if(!string_to_int64(seconds_str, &seconds) || seconds <= 0 || seconds > INT_MAX) { + gsr_log(GSR_LOG_LEVEL_ERROR, "expected the number of seconds to save to be an integer larger than 0, got: '%s'", seconds_str); + return 1; + } + snprintf(request, sizeof(request), "{\"id\":%d,\"name\":\"%s\",\"data\":%" PRIi64 "}\n", GSR_CLI_REQUEST_ID, name, seconds); + } else { + snprintf(request, sizeof(request), "{\"id\":%d,\"name\":\"%s\"}\n", GSR_CLI_REQUEST_ID, name); + } + + const int fd = ipc_connect(socket_filepath); + if(fd == -1) { + gsr_log(GSR_LOG_LEVEL_ERROR, "failed to connect to \"%s\". Is GPU Screen Recorder running with the -ipc option?", socket_filepath); + return 1; + } + + int exit_code = 1; + char reply[GSR_CLI_MAX_REPLY_SIZE]; + size_t reply_size = 0; + if(ipc_send_all(fd, request, strlen(request)) && ipc_receive_reply(fd, reply, sizeof(reply), &reply_size)) + exit_code = ipc_handle_reply(reply, reply_size, GSR_CLI_REQUEST_ID); + + close(fd); + return exit_code; +} + +int main(int argc, char **argv) { + if(argc == 2 && (strcmp(argv[1], "-h") == 0 || strcmp(argv[1], "--help") == 0)) { + usage(); + return 0; + } + + if(argc < 4 || strcmp(argv[1], "-ipc") != 0) { + usage(); + return 1; + } + + if(argc > 5) { + gsr_log(GSR_LOG_LEVEL_ERROR, "too many arguments"); + usage(); + return 1; + } + + const char *socket_filepath = argv[2]; + const char *command = argv[3]; + const char *command_argument = argc == 5 ? argv[4] : NULL; + + if(strcmp(command, "save-replay") == 0) + return send_command(socket_filepath, command, command_argument); + + if(command_argument) { + gsr_log(GSR_LOG_LEVEL_ERROR, "the '%s' command doesn't take an argument", command); + usage(); + return 1; + } + + if(strcmp(command, "status") == 0) + return status_command(socket_filepath); + + if(strcmp(command, "stop") == 0 || strcmp(command, "toggle-pause") == 0 || strcmp(command, "toggle-replay-recording") == 0) + return send_command(socket_filepath, command, NULL); + + gsr_log(GSR_LOG_LEVEL_ERROR, "invalid command '%s'", command); + usage(); + return 1; +} diff --git a/src/json.c b/src/json.c new file mode 100644 index 0000000..f7d1cd7 --- /dev/null +++ b/src/json.c @@ -0,0 +1,69 @@ +#include "../include/json.h" + +#define SJ_IMPL +#include "../external/sj.h" + +#include +#include +#include +#include + +bool gsr_json_string_equals(const sj_Value *value, const char *str) { + const size_t value_size = value->end - value->start; + return strlen(str) == value_size && memcmp(value->start, str, value_size) == 0; +} + +bool gsr_json_number_to_int64(const sj_Value *value, int64_t *result) { + if(value->type != SJ_NUMBER) + return false; + + char buffer[32]; + const size_t value_size = value->end - value->start; + if(value_size == 0 || value_size >= sizeof(buffer)) + return false; + + memcpy(buffer, value->start, value_size); + buffer[value_size] = '\0'; + + char *number_end = NULL; + errno = 0; + const long long parsed_value = strtoll(buffer, &number_end, 10); + if(errno != 0 || number_end != buffer + value_size) + return false; + + *result = parsed_value; + return true; +} + +void gsr_json_escape_string(char *buffer, size_t buffer_size, const char *str) { + char escape_buffer[8]; + size_t offset = 0; + buffer[0] = '\0'; + + for(size_t i = 0; str[i] != '\0'; ++i) { + const unsigned char c = str[i]; + const char *escaped = escape_buffer; + switch(c) { + case '"': escaped = "\\\""; break; + case '\\': escaped = "\\\\"; break; + case '\n': escaped = "\\n"; break; + case '\r': escaped = "\\r"; break; + case '\t': escaped = "\\t"; break; + default: { + if(c < 0x20) + snprintf(escape_buffer, sizeof(escape_buffer), "\\u%04x", c); + else + snprintf(escape_buffer, sizeof(escape_buffer), "%c", c); + break; + } + } + + const size_t escaped_size = strlen(escaped); + if(offset + escaped_size >= buffer_size) + break; + + memcpy(buffer + offset, escaped, escaped_size); + offset += escaped_size; + buffer[offset] = '\0'; + } +} -- cgit v1.2.3 From 8fc8aaa7993d071955cb56dfda70555d953da916 Mon Sep 17 00:00:00 2001 From: dec05eba Date: Sun, 2 Aug 2026 02:03:21 +0200 Subject: Document the ipc option next to the signals in the readme The remote control section is now split into a signals part and an ipc part with gsr-cli examples, and it points at the gsr-cli man page and the IPC section of the gpu-screen-recorder man page. --- README.md | 29 ++++++++++++++++++++++++----- src/recorder/recorder.c | 2 +- 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 1b7502d..26e59e5 100644 --- a/README.md +++ b/README.md @@ -135,14 +135,33 @@ You can record a regular video while using replay/streaming by launching GPU Scr To start/stop (and save) recording use the SIGRTMIN signal, for example `pkill -SIGRTMIN -f "^gpu-screen-recorder"`. The path to the video will be displayed in stdout when saving the video.\ This way of recording while using replay/streaming is more efficient than running GPU Screen Recorder multiple times since this way it only records the screen and encodes the video once. ## Controlling GPU Screen Recorder remotely +GPU Screen Recorder can be controlled with signals, or with commands over a unix domain socket when it's started with the `-ipc` option. +### Signals To save a video in replay mode, you need to send signal SIGUSR1 to gpu screen recorder. You can do this by running `pkill -SIGUSR1 -f "^gpu-screen-recorder"`.\ To stop recording send SIGINT to gpu screen recorder. You can do this by running `pkill -SIGINT -f "^gpu-screen-recorder"` or pressing `Ctrl-C` in the terminal that runs gpu screen recorder. When recording a regular non-replay video this will also save the video.\ To pause/unpause recording send SIGUSR2 to gpu screen recorder. You can do this by running `pkill -SIGUSR2 -f "^gpu-screen-recorder"`. This is only applicable and useful when recording (not streaming nor replay).\ -There are more signals to control GPU Screen Recorder. Run `gpu-screen-recorder --help` to list them all (under `NOTES` section).\ -GPU Screen Recorder can also be controlled with json messages over a unix domain socket by launching it with the `-ipc` option, for example `-ipc "$XDG_RUNTIME_DIR/gsr.sock"`. -This gives the same control as the signals, except that a replay can be saved with an arbitrary number of seconds and that you get a reply that says if the command succeeded.\ -The `gsr-cli` program sends these commands, for example `gsr-cli -ipc "$XDG_RUNTIME_DIR/gsr.sock" save-replay 30`. It can also check if GPU Screen Recorder is running with `gsr-cli -ipc "$XDG_RUNTIME_DIR/gsr.sock" status`.\ -See `man gsr-cli` for all commands and the `IPC` section in `man gpu-screen-recorder` for the protocol, if you want to talk to the socket directly instead of using `gsr-cli`. +There are more signals to control GPU Screen Recorder. Run `gpu-screen-recorder --help` to list them all (under `NOTES` section). +### IPC +Start GPU Screen Recorder with the `-ipc` option to also make it listen for commands on a unix domain socket, for example:\ +``` +gpu-screen-recorder -w screen -f 60 -c mp4 -r 60 -o ~/Videos -ipc "$XDG_RUNTIME_DIR/gsr.sock" +``` +and then use the `gsr-cli` program to send commands to it:\ +``` +gsr-cli -ipc "$XDG_RUNTIME_DIR/gsr.sock" save-replay 30 +gsr-cli -ipc "$XDG_RUNTIME_DIR/gsr.sock" save-replay +gsr-cli -ipc "$XDG_RUNTIME_DIR/gsr.sock" toggle-replay-recording +gsr-cli -ipc "$XDG_RUNTIME_DIR/gsr.sock" toggle-pause +gsr-cli -ipc "$XDG_RUNTIME_DIR/gsr.sock" stop +gsr-cli -ipc "$XDG_RUNTIME_DIR/gsr.sock" status +``` +This gives the same control as the signals, with these differences: +* `save-replay` takes the number of seconds to save, instead of the fixed times that the signals provide. The whole replay buffer is saved when no number of seconds is given. +* `gsr-cli` exits with 0 only when the command succeeded and prints the reason to stderr when it didn't, so commands don't have to be sent blindly. +* `status` prints `running` or `not running` and exits with 0 when GPU Screen Recorder is running, which a script can use to only start replay when it isn't already running: `gsr-cli -ipc "$XDG_RUNTIME_DIR/gsr.sock" status >/dev/null || start-replay.sh`. +* The commands are sent to one specific GPU Screen Recorder instance instead of every instance that `pkill` matches. + +Run `man gsr-cli` to see all commands. The commands are newline terminated json messages, which are described in the `IPC` section of `man gpu-screen-recorder` if you want to talk to the socket directly instead of using `gsr-cli`. ## Simple way to run replay without gui Run the script `scripts/start-replay.sh` to start replay and then `scripts/save-replay.sh` to save a replay and `scripts/stop-replay.sh` to stop the replay. The videos are saved to `$HOME/Videos`. You can use these scripts to start replay at system startup if you add `scripts/start-replay.sh` to startup (this can be done differently depending on your desktop environment / window manager) and then go into diff --git a/src/recorder/recorder.c b/src/recorder/recorder.c index 6f9ea12..4426ac5 100644 --- a/src/recorder/recorder.c +++ b/src/recorder/recorder.c @@ -511,7 +511,7 @@ static void recorder_update_fps_counters(gsr_recorder *self) { const double elapsed = time_now - self->fps_start_time; if (elapsed >= 1.0) { if(self->settings.verbose) { - fprintf(stderr, "update fps: %d, damage fps: %d\n", self->fps_counter, self->damage_fps_counter); + gsr_log(GSR_LOG_LEVEL_INFO, "update fps: %d, damage fps: %d", self->fps_counter, self->damage_fps_counter); } self->fps_start_time = time_now; self->fps_counter = 0; -- cgit v1.2.3 From f39ebf1a607b2f45c5049560756fb0f0054c5f6f Mon Sep 17 00:00:00 2001 From: dec05eba Date: Sun, 2 Aug 2026 20:12:20 +0200 Subject: Move gsr-cli to tools/gsr-cli --- include/sound.h | 17 --- meson.build | 2 +- src/cli/main.c | 17 --- src/gsr_cli/main.c | 298 --------------------------------------------------- tools/gsr-cli/main.c | 281 ++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 282 insertions(+), 333 deletions(-) delete mode 100644 src/gsr_cli/main.c create mode 100644 tools/gsr-cli/main.c diff --git a/include/sound.h b/include/sound.h index eaa1236..31c7d94 100644 --- a/include/sound.h +++ b/include/sound.h @@ -1,20 +1,3 @@ -/* - Copyright (C) 2020 dec05eba - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . -*/ - #ifndef GSR_SOUND_H #define GSR_SOUND_H diff --git a/meson.build b/meson.build index 7b0c14e..bebfa3d 100644 --- a/meson.build +++ b/meson.build @@ -132,7 +132,7 @@ endif add_project_arguments('-DGSR_VERSION="' + meson.project_version() + '"', language: 'c') executable('gsr-kms-server', 'kms/server/kms_server.c', dependencies : dependency('libdrm'), c_args : '-fstack-protector-all', install : true) -executable('gsr-cli', ['src/gsr_cli/main.c', 'src/json.c', 'src/log.c'], install : true) +executable('gsr-cli', ['tools/gsr-cli/main.c', 'src/json.c', 'src/log.c'], install : true) executable('gpu-screen-recorder', src, dependencies : dep, install : true) install_headers('plugin/plugin.h', install_dir : 'include/gsr') diff --git a/src/cli/main.c b/src/cli/main.c index 5ead4a4..d34e8cc 100644 --- a/src/cli/main.c +++ b/src/cli/main.c @@ -1,20 +1,3 @@ -/* - Copyright (C) 2020 dec05eba - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . -*/ - #include "../../include/cli/commands.h" #include "../../include/cli/ipc.h" #include "../../include/recorder/recorder.h" diff --git a/src/gsr_cli/main.c b/src/gsr_cli/main.c deleted file mode 100644 index fb0fcbf..0000000 --- a/src/gsr_cli/main.c +++ /dev/null @@ -1,298 +0,0 @@ -/* - Copyright (C) 2020 dec05eba - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . -*/ - -#include "../../include/json.h" -#include "../../include/log.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#define GSR_CLI_REQUEST_ID 1 -#define GSR_CLI_MAX_REQUEST_SIZE 256 -#define GSR_CLI_MAX_REPLY_SIZE 4096 -#define GSR_CLI_REPLY_TIMEOUT_SECONDS 10 - -static void usage(void) { - printf("usage: gsr-cli -ipc [command_argument]\n"); - printf("\n"); - printf("Sends a command to a GPU Screen Recorder instance that was started with the -ipc option.\n"); - printf("\n"); - printf("OPTIONS:\n"); - printf(" -ipc \n"); - printf(" The unix domain socket that GPU Screen Recorder was started with. Required.\n"); - printf("\n"); - printf("COMMANDS:\n"); - printf(" status\n"); - printf(" Check if a GPU Screen Recorder instance is listening on the socket. Prints \"running\" or\n"); - printf(" \"not running\" and exits with 0 when it's running.\n"); - printf(" stop\n"); - printf(" Stop and save the recording (stop without save in replay mode).\n"); - printf(" toggle-pause\n"); - printf(" Pause/unpause the recording (not for streaming/replay).\n"); - printf(" toggle-replay-recording\n"); - printf(" Start/stop a regular recording during replay/streaming.\n"); - printf(" save-replay [seconds]\n"); - printf(" Save the replay. The number of seconds has to be larger than 0. The whole replay buffer is\n"); - printf(" saved when no number of seconds is given.\n"); - printf("\n"); - printf("EXAMPLES:\n"); - printf(" gsr-cli -ipc \"$XDG_RUNTIME_DIR/gsr.sock\" status\n"); - printf(" gsr-cli -ipc \"$XDG_RUNTIME_DIR/gsr.sock\" save-replay 30\n"); - fflush(stdout); -} - -static bool string_to_int64(const char *str, int64_t *result) { - char *number_end = NULL; - errno = 0; - const long long parsed_value = strtoll(str, &number_end, 10); - if(errno != 0 || number_end == str || *number_end != '\0') - return false; - - *result = parsed_value; - return true; -} - -/* Returns the socket, or -1 on failure. Only logs an error when the failure isn't a missing GPU Screen Recorder instance */ -static int ipc_connect(const char *socket_filepath) { - 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, "the ipc socket path is too long, it can be at most %d characters: \"%s\"", (int)sizeof(addr.sun_path) - 1, socket_filepath); - return -1; - } - - const int fd = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0); - if(fd == -1) { - gsr_log(GSR_LOG_LEVEL_ERROR, "failed to create a socket, error: %s", strerror(errno)); - return -1; - } - - struct timeval timeout; - timeout.tv_sec = GSR_CLI_REPLY_TIMEOUT_SECONDS; - timeout.tv_usec = 0; - setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout)); - setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &timeout, sizeof(timeout)); - - if(connect(fd, (const struct sockaddr*)&addr, sizeof(addr)) == -1) { - close(fd); - return -1; - } - - return fd; -} - -static bool ipc_send_all(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; - - gsr_log(GSR_LOG_LEVEL_ERROR, "failed to send the request, error: %s", strerror(errno)); - return false; - } - return true; -} - -/* Reads until a newline. |reply_size| is set to the size of the reply, excluding the newline */ -static bool ipc_receive_reply(int fd, char *reply, size_t reply_capacity, size_t *reply_size) { - size_t offset = 0; - for(;;) { - if(offset == reply_capacity) { - gsr_log(GSR_LOG_LEVEL_ERROR, "the reply is too large"); - return false; - } - - const ssize_t bytes_read = recv(fd, reply + offset, reply_capacity - offset, 0); - if(bytes_read == 0) { - gsr_log(GSR_LOG_LEVEL_ERROR, "GPU Screen Recorder closed the connection before replying"); - return false; - } - - if(bytes_read == -1) { - if(errno == EINTR) - continue; - - if(errno == EAGAIN || errno == EWOULDBLOCK) - gsr_log(GSR_LOG_LEVEL_ERROR, "timed out after %d seconds waiting for a reply", GSR_CLI_REPLY_TIMEOUT_SECONDS); - else - gsr_log(GSR_LOG_LEVEL_ERROR, "failed to receive the reply, error: %s", strerror(errno)); - return false; - } - - const char *newline = memchr(reply + offset, '\n', bytes_read); - offset += bytes_read; - if(newline) { - *reply_size = newline - reply; - return true; - } - } -} - -/* Returns the exit code that gsr-cli should exit with */ -static int ipc_handle_reply(char *reply, size_t reply_size, int64_t request_id) { - sj_Reader reader = sj_reader(reply, reply_size); - const sj_Value root = sj_read(&reader); - if(root.type != SJ_OBJECT) { - gsr_log(GSR_LOG_LEVEL_ERROR, "expected the reply to be a json object, got: %.*s", (int)reply_size, reply); - return 1; - } - - int64_t id = 0; - bool has_id = false; - sj_Value result_value; - bool has_result = false; - sj_Value data_value; - bool has_data = false; - - sj_Value key; - sj_Value value; - while(sj_iter_object(&reader, root, &key, &value)) { - if(gsr_json_string_equals(&key, "id")) { - has_id = gsr_json_number_to_int64(&value, &id); - } else if(gsr_json_string_equals(&key, "result")) { - result_value = value; - has_result = value.type == SJ_STRING; - } else if(gsr_json_string_equals(&key, "data")) { - data_value = value; - has_data = true; - } - } - - if(reader.error) { - gsr_log(GSR_LOG_LEVEL_ERROR, "failed to parse the reply: %s", reader.error); - return 1; - } - - if(!has_id || id != request_id) { - gsr_log(GSR_LOG_LEVEL_ERROR, "received a reply to another request: %.*s", (int)reply_size, reply); - return 1; - } - - if(!has_result) { - gsr_log(GSR_LOG_LEVEL_ERROR, "the reply is missing the 'result' field: %.*s", (int)reply_size, reply); - return 1; - } - - if(gsr_json_string_equals(&result_value, "ok")) - return 0; - - if(has_data && data_value.type == SJ_STRING) - gsr_log(GSR_LOG_LEVEL_ERROR, "%.*s", (int)(data_value.end - data_value.start), data_value.start); - else - gsr_log(GSR_LOG_LEVEL_ERROR, "the request failed: %.*s", (int)reply_size, reply); - - return 1; -} - -static int status_command(const char *socket_filepath) { - const int fd = ipc_connect(socket_filepath); - if(fd == -1) { - printf("not running\n"); - fflush(stdout); - return 1; - } - - close(fd); - printf("running\n"); - fflush(stdout); - return 0; -} - -static int send_command(const char *socket_filepath, const char *name, const char *seconds_str) { - char request[GSR_CLI_MAX_REQUEST_SIZE]; - if(seconds_str) { - int64_t seconds = 0; - if(!string_to_int64(seconds_str, &seconds) || seconds <= 0 || seconds > INT_MAX) { - gsr_log(GSR_LOG_LEVEL_ERROR, "expected the number of seconds to save to be an integer larger than 0, got: '%s'", seconds_str); - return 1; - } - snprintf(request, sizeof(request), "{\"id\":%d,\"name\":\"%s\",\"data\":%" PRIi64 "}\n", GSR_CLI_REQUEST_ID, name, seconds); - } else { - snprintf(request, sizeof(request), "{\"id\":%d,\"name\":\"%s\"}\n", GSR_CLI_REQUEST_ID, name); - } - - const int fd = ipc_connect(socket_filepath); - if(fd == -1) { - gsr_log(GSR_LOG_LEVEL_ERROR, "failed to connect to \"%s\". Is GPU Screen Recorder running with the -ipc option?", socket_filepath); - return 1; - } - - int exit_code = 1; - char reply[GSR_CLI_MAX_REPLY_SIZE]; - size_t reply_size = 0; - if(ipc_send_all(fd, request, strlen(request)) && ipc_receive_reply(fd, reply, sizeof(reply), &reply_size)) - exit_code = ipc_handle_reply(reply, reply_size, GSR_CLI_REQUEST_ID); - - close(fd); - return exit_code; -} - -int main(int argc, char **argv) { - if(argc == 2 && (strcmp(argv[1], "-h") == 0 || strcmp(argv[1], "--help") == 0)) { - usage(); - return 0; - } - - if(argc < 4 || strcmp(argv[1], "-ipc") != 0) { - usage(); - return 1; - } - - if(argc > 5) { - gsr_log(GSR_LOG_LEVEL_ERROR, "too many arguments"); - usage(); - return 1; - } - - const char *socket_filepath = argv[2]; - const char *command = argv[3]; - const char *command_argument = argc == 5 ? argv[4] : NULL; - - if(strcmp(command, "save-replay") == 0) - return send_command(socket_filepath, command, command_argument); - - if(command_argument) { - gsr_log(GSR_LOG_LEVEL_ERROR, "the '%s' command doesn't take an argument", command); - usage(); - return 1; - } - - if(strcmp(command, "status") == 0) - return status_command(socket_filepath); - - if(strcmp(command, "stop") == 0 || strcmp(command, "toggle-pause") == 0 || strcmp(command, "toggle-replay-recording") == 0) - return send_command(socket_filepath, command, NULL); - - gsr_log(GSR_LOG_LEVEL_ERROR, "invalid command '%s'", command); - usage(); - return 1; -} diff --git a/tools/gsr-cli/main.c b/tools/gsr-cli/main.c new file mode 100644 index 0000000..b536ceb --- /dev/null +++ b/tools/gsr-cli/main.c @@ -0,0 +1,281 @@ +#include "../../include/json.h" +#include "../../include/log.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define GSR_CLI_REQUEST_ID 1 +#define GSR_CLI_MAX_REQUEST_SIZE 256 +#define GSR_CLI_MAX_REPLY_SIZE 4096 +#define GSR_CLI_REPLY_TIMEOUT_SECONDS 10 + +static void usage(void) { + printf("usage: gsr-cli -ipc [command_argument]\n"); + printf("\n"); + printf("Sends a command to a GPU Screen Recorder instance that was started with the -ipc option.\n"); + printf("\n"); + printf("OPTIONS:\n"); + printf(" -ipc \n"); + printf(" The unix domain socket that GPU Screen Recorder was started with. Required.\n"); + printf("\n"); + printf("COMMANDS:\n"); + printf(" status\n"); + printf(" Check if a GPU Screen Recorder instance is listening on the socket. Prints \"running\" or\n"); + printf(" \"not running\" and exits with 0 when it's running.\n"); + printf(" stop\n"); + printf(" Stop and save the recording (stop without save in replay mode).\n"); + printf(" toggle-pause\n"); + printf(" Pause/unpause the recording (not for streaming/replay).\n"); + printf(" toggle-replay-recording\n"); + printf(" Start/stop a regular recording during replay/streaming.\n"); + printf(" save-replay [seconds]\n"); + printf(" Save the replay. The number of seconds has to be larger than 0. The whole replay buffer is\n"); + printf(" saved when no number of seconds is given.\n"); + printf("\n"); + printf("EXAMPLES:\n"); + printf(" gsr-cli -ipc \"$XDG_RUNTIME_DIR/gsr.sock\" status\n"); + printf(" gsr-cli -ipc \"$XDG_RUNTIME_DIR/gsr.sock\" save-replay 30\n"); + fflush(stdout); +} + +static bool string_to_int64(const char *str, int64_t *result) { + char *number_end = NULL; + errno = 0; + const long long parsed_value = strtoll(str, &number_end, 10); + if(errno != 0 || number_end == str || *number_end != '\0') + return false; + + *result = parsed_value; + return true; +} + +/* Returns the socket, or -1 on failure. Only logs an error when the failure isn't a missing GPU Screen Recorder instance */ +static int ipc_connect(const char *socket_filepath) { + 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, "the ipc socket path is too long, it can be at most %d characters: \"%s\"", (int)sizeof(addr.sun_path) - 1, socket_filepath); + return -1; + } + + const int fd = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0); + if(fd == -1) { + gsr_log(GSR_LOG_LEVEL_ERROR, "failed to create a socket, error: %s", strerror(errno)); + return -1; + } + + struct timeval timeout; + timeout.tv_sec = GSR_CLI_REPLY_TIMEOUT_SECONDS; + timeout.tv_usec = 0; + setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout)); + setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &timeout, sizeof(timeout)); + + if(connect(fd, (const struct sockaddr*)&addr, sizeof(addr)) == -1) { + close(fd); + return -1; + } + + return fd; +} + +static bool ipc_send_all(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; + + gsr_log(GSR_LOG_LEVEL_ERROR, "failed to send the request, error: %s", strerror(errno)); + return false; + } + return true; +} + +/* Reads until a newline. |reply_size| is set to the size of the reply, excluding the newline */ +static bool ipc_receive_reply(int fd, char *reply, size_t reply_capacity, size_t *reply_size) { + size_t offset = 0; + for(;;) { + if(offset == reply_capacity) { + gsr_log(GSR_LOG_LEVEL_ERROR, "the reply is too large"); + return false; + } + + const ssize_t bytes_read = recv(fd, reply + offset, reply_capacity - offset, 0); + if(bytes_read == 0) { + gsr_log(GSR_LOG_LEVEL_ERROR, "GPU Screen Recorder closed the connection before replying"); + return false; + } + + if(bytes_read == -1) { + if(errno == EINTR) + continue; + + if(errno == EAGAIN || errno == EWOULDBLOCK) + gsr_log(GSR_LOG_LEVEL_ERROR, "timed out after %d seconds waiting for a reply", GSR_CLI_REPLY_TIMEOUT_SECONDS); + else + gsr_log(GSR_LOG_LEVEL_ERROR, "failed to receive the reply, error: %s", strerror(errno)); + return false; + } + + const char *newline = memchr(reply + offset, '\n', bytes_read); + offset += bytes_read; + if(newline) { + *reply_size = newline - reply; + return true; + } + } +} + +/* Returns the exit code that gsr-cli should exit with */ +static int ipc_handle_reply(char *reply, size_t reply_size, int64_t request_id) { + sj_Reader reader = sj_reader(reply, reply_size); + const sj_Value root = sj_read(&reader); + if(root.type != SJ_OBJECT) { + gsr_log(GSR_LOG_LEVEL_ERROR, "expected the reply to be a json object, got: %.*s", (int)reply_size, reply); + return 1; + } + + int64_t id = 0; + bool has_id = false; + sj_Value result_value; + bool has_result = false; + sj_Value data_value; + bool has_data = false; + + sj_Value key; + sj_Value value; + while(sj_iter_object(&reader, root, &key, &value)) { + if(gsr_json_string_equals(&key, "id")) { + has_id = gsr_json_number_to_int64(&value, &id); + } else if(gsr_json_string_equals(&key, "result")) { + result_value = value; + has_result = value.type == SJ_STRING; + } else if(gsr_json_string_equals(&key, "data")) { + data_value = value; + has_data = true; + } + } + + if(reader.error) { + gsr_log(GSR_LOG_LEVEL_ERROR, "failed to parse the reply: %s", reader.error); + return 1; + } + + if(!has_id || id != request_id) { + gsr_log(GSR_LOG_LEVEL_ERROR, "received a reply to another request: %.*s", (int)reply_size, reply); + return 1; + } + + if(!has_result) { + gsr_log(GSR_LOG_LEVEL_ERROR, "the reply is missing the 'result' field: %.*s", (int)reply_size, reply); + return 1; + } + + if(gsr_json_string_equals(&result_value, "ok")) + return 0; + + if(has_data && data_value.type == SJ_STRING) + gsr_log(GSR_LOG_LEVEL_ERROR, "%.*s", (int)(data_value.end - data_value.start), data_value.start); + else + gsr_log(GSR_LOG_LEVEL_ERROR, "the request failed: %.*s", (int)reply_size, reply); + + return 1; +} + +static int status_command(const char *socket_filepath) { + const int fd = ipc_connect(socket_filepath); + if(fd == -1) { + printf("not running\n"); + fflush(stdout); + return 1; + } + + close(fd); + printf("running\n"); + fflush(stdout); + return 0; +} + +static int send_command(const char *socket_filepath, const char *name, const char *seconds_str) { + char request[GSR_CLI_MAX_REQUEST_SIZE]; + if(seconds_str) { + int64_t seconds = 0; + if(!string_to_int64(seconds_str, &seconds) || seconds <= 0 || seconds > INT_MAX) { + gsr_log(GSR_LOG_LEVEL_ERROR, "expected the number of seconds to save to be an integer larger than 0, got: '%s'", seconds_str); + return 1; + } + snprintf(request, sizeof(request), "{\"id\":%d,\"name\":\"%s\",\"data\":%" PRIi64 "}\n", GSR_CLI_REQUEST_ID, name, seconds); + } else { + snprintf(request, sizeof(request), "{\"id\":%d,\"name\":\"%s\"}\n", GSR_CLI_REQUEST_ID, name); + } + + const int fd = ipc_connect(socket_filepath); + if(fd == -1) { + gsr_log(GSR_LOG_LEVEL_ERROR, "failed to connect to \"%s\". Is GPU Screen Recorder running with the -ipc option?", socket_filepath); + return 1; + } + + int exit_code = 1; + char reply[GSR_CLI_MAX_REPLY_SIZE]; + size_t reply_size = 0; + if(ipc_send_all(fd, request, strlen(request)) && ipc_receive_reply(fd, reply, sizeof(reply), &reply_size)) + exit_code = ipc_handle_reply(reply, reply_size, GSR_CLI_REQUEST_ID); + + close(fd); + return exit_code; +} + +int main(int argc, char **argv) { + if(argc == 2 && (strcmp(argv[1], "-h") == 0 || strcmp(argv[1], "--help") == 0)) { + usage(); + return 0; + } + + if(argc < 4 || strcmp(argv[1], "-ipc") != 0) { + usage(); + return 1; + } + + if(argc > 5) { + gsr_log(GSR_LOG_LEVEL_ERROR, "too many arguments"); + usage(); + return 1; + } + + const char *socket_filepath = argv[2]; + const char *command = argv[3]; + const char *command_argument = argc == 5 ? argv[4] : NULL; + + if(strcmp(command, "save-replay") == 0) + return send_command(socket_filepath, command, command_argument); + + if(command_argument) { + gsr_log(GSR_LOG_LEVEL_ERROR, "the '%s' command doesn't take an argument", command); + usage(); + return 1; + } + + if(strcmp(command, "status") == 0) + return status_command(socket_filepath); + + if(strcmp(command, "stop") == 0 || strcmp(command, "toggle-pause") == 0 || strcmp(command, "toggle-replay-recording") == 0) + return send_command(socket_filepath, command, NULL); + + gsr_log(GSR_LOG_LEVEL_ERROR, "invalid command '%s'", command); + usage(); + return 1; +} -- cgit v1.2.3 From 8397e7307eb68c857117d74ba332916bb7af6be4 Mon Sep 17 00:00:00 2001 From: Walter McKelvie Date: Sun, 2 Aug 2026 20:19:32 +0200 Subject: feat: Apple Silicon support --- include/defs.h | 1 + src/cli/commands.c | 3 +++ src/recorder/codec_select.c | 1 + src/utils.c | 5 ++++- 4 files changed, 9 insertions(+), 1 deletion(-) diff --git a/include/defs.h b/include/defs.h index 4bb10d8..5ffb546 100644 --- a/include/defs.h +++ b/include/defs.h @@ -11,6 +11,7 @@ typedef enum { GSR_GPU_VENDOR_INTEL, GSR_GPU_VENDOR_NVIDIA, GSR_GPU_VENDOR_BROADCOM, + GSR_GPU_VENDOR_APPLE } gsr_gpu_vendor; typedef struct { diff --git a/src/cli/commands.c b/src/cli/commands.c index 12de13b..bf07285 100644 --- a/src/cli/commands.c +++ b/src/cli/commands.c @@ -53,6 +53,9 @@ static void list_gpu_info(gsr_egl *egl) { 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); } diff --git a/src/recorder/codec_select.c b/src/recorder/codec_select.c index 48af0b2..38f7c61 100644 --- a/src/recorder/codec_select.c +++ b/src/recorder/codec_select.c @@ -71,6 +71,7 @@ bool get_supported_video_codecs(gsr_egl *egl, gsr_video_codec video_codec, bool case GSR_GPU_VENDOR_AMD: case GSR_GPU_VENDOR_INTEL: case GSR_GPU_VENDOR_BROADCOM: + case GSR_GPU_VENDOR_APPLE: return gsr_get_supported_video_codecs_vaapi(video_codecs, egl->card_path, cleanup); case GSR_GPU_VENDOR_NVIDIA: return gsr_get_supported_video_codecs_nvenc(video_codecs, cleanup); diff --git a/src/utils.c b/src/utils.c index 9ae34d7..71ad02d 100644 --- a/src/utils.c +++ b/src/utils.c @@ -389,6 +389,8 @@ bool gl_get_gpu_info(gsr_egl *egl, gsr_gpu_info *info) { info->vendor = GSR_GPU_VENDOR_NVIDIA; else if(strstr(gl_vendor, "Broadcom")) info->vendor = GSR_GPU_VENDOR_BROADCOM; + else if(strstr(gl_vendor, "Mesa") && gl_renderer && strstr(gl_renderer, "Apple")) + info->vendor = GSR_GPU_VENDOR_APPLE; else { gsr_log(GSR_LOG_LEVEL_ERROR, "unknown gpu vendor: %s", gl_vendor); return false; @@ -450,7 +452,8 @@ bool try_card_has_valid_plane(const char *card_path) { bool gsr_get_valid_card_path(gsr_egl *egl, char *output, bool is_monitor_capture) { if(egl->dri_card_path) { snprintf(output, 128, "%s", egl->dri_card_path); - return is_monitor_capture ? try_card_has_valid_plane(output) : true; + if(!is_monitor_capture || try_card_has_valid_plane(output)) + return true; } for(int i = 0; i < 10; ++i) { -- cgit v1.2.3 From ca4fabc1d67b8d253d6c476fa47668c01288e20f Mon Sep 17 00:00:00 2001 From: dec05eba Date: Sun, 2 Aug 2026 20:26:43 +0200 Subject: Set c standard to c11 --- meson.build | 2 +- project.conf | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/meson.build b/meson.build index bebfa3d..41a5d38 100644 --- a/meson.build +++ b/meson.build @@ -1,4 +1,4 @@ -project('gpu-screen-recorder', 'c', version : '5.15.3', default_options : ['warning_level=2']) +project('gpu-screen-recorder', 'c', version : '5.15.3', default_options : ['c_std=c11', 'warning_level=2']) add_project_arguments('-Wshadow', language : 'c') if get_option('buildtype') == 'debug' diff --git a/project.conf b/project.conf index 38af0ba..f57b88b 100644 --- a/project.conf +++ b/project.conf @@ -4,11 +4,11 @@ type = "executable" version = "5.15.3" platforms = ["posix"] -[lang.cpp] -version = "c++17" +[lang.c] +version = "c11" [config] -ignore_dirs = ["kms/server", "src/gsr_cli", "build", "debug-build", "plugin/examples"] +ignore_dirs = ["kms/server", "tools", "build", "debug-build", "plugin/examples"] #error_on_warning = "true" [define] -- cgit v1.2.3 From bf9213b3a884ed60959585dce423c1d6acc7aeeb Mon Sep 17 00:00:00 2001 From: dec05eba Date: Sun, 2 Aug 2026 20:53:47 +0200 Subject: Fix --info command on applel --- src/cli/commands.c | 6 +++--- src/recorder/codec_select.c | 3 ++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/cli/commands.c b/src/cli/commands.c index bf07285..9b77f79 100644 --- a/src/cli/commands.c +++ b/src/cli/commands.c @@ -238,7 +238,7 @@ int version_command(void *userdata) { int info_command(void *userdata) { (void)userdata; gsr_windowing windowing; - const gsr_windowing_params windowing_params = {false, false, false}; + const gsr_windowing_params windowing_params = { .monitor_capture = true }; if(gsr_windowing_init(&windowing, &windowing_params) != GSR_ERROR_OK) return 1; @@ -326,7 +326,7 @@ int list_v4l2_devices(void *userdata) { int list_capture_options_command(const char *card_path, void *userdata) { (void)userdata; gsr_windowing windowing; - const gsr_windowing_params windowing_params = {false, false, false}; + const gsr_windowing_params windowing_params = { .monitor_capture = true }; if(gsr_windowing_init(&windowing, &windowing_params) != GSR_ERROR_OK) return 1; @@ -348,7 +348,7 @@ int list_capture_options_command(const char *card_path, void *userdata) { int list_monitors_command(void *userdata) { (void)userdata; gsr_windowing windowing; - const gsr_windowing_params windowing_params = {false, false, false}; + const gsr_windowing_params windowing_params = { .monitor_capture = true }; if(gsr_windowing_init(&windowing, &windowing_params) != GSR_ERROR_OK) return 1; diff --git a/src/recorder/codec_select.c b/src/recorder/codec_select.c index 38f7c61..5f2bb68 100644 --- a/src/recorder/codec_select.c +++ b/src/recorder/codec_select.c @@ -36,7 +36,8 @@ gsr_video_encoder* create_video_encoder(gsr_egl *egl, const gsr_recorder_setting switch(egl->gpu_info.vendor) { case GSR_GPU_VENDOR_AMD: case GSR_GPU_VENDOR_INTEL: - case GSR_GPU_VENDOR_BROADCOM: { + case GSR_GPU_VENDOR_BROADCOM: + case GSR_GPU_VENDOR_APPLE: { gsr_video_encoder_vaapi_params params; params.egl = egl; params.color_depth = color_depth; -- cgit v1.2.3 From 1869cf30519fc61c7f9852f8f25f8b1c2b2e8c3b Mon Sep 17 00:00:00 2001 From: dec05eba Date: Sun, 2 Aug 2026 21:21:32 +0200 Subject: Fix build because of c version --- meson.build | 2 +- project.conf | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/meson.build b/meson.build index 41a5d38..8e9d8a4 100644 --- a/meson.build +++ b/meson.build @@ -1,4 +1,4 @@ -project('gpu-screen-recorder', 'c', version : '5.15.3', default_options : ['c_std=c11', 'warning_level=2']) +project('gpu-screen-recorder', 'c', version : '5.15.3', default_options : ['c_std=gnu11', 'warning_level=2']) add_project_arguments('-Wshadow', language : 'c') if get_option('buildtype') == 'debug' diff --git a/project.conf b/project.conf index f57b88b..e3895a2 100644 --- a/project.conf +++ b/project.conf @@ -4,9 +4,6 @@ type = "executable" version = "5.15.3" platforms = ["posix"] -[lang.c] -version = "c11" - [config] ignore_dirs = ["kms/server", "tools", "build", "debug-build", "plugin/examples"] #error_on_warning = "true" -- cgit v1.2.3 From b9d252214a46aa64047893cb557f060a8978b006 Mon Sep 17 00:00:00 2001 From: dec05eba Date: Wed, 5 Aug 2026 02:06:11 +0200 Subject: Add option to statically compile ffmpeg from source --- .gitignore | 4 + README.md | 8 + extra/build_ffmpeg.sh | 233 ++++++ meson.build | 5 + meson_options.txt | 1 + subprojects/ffmpeg.wrap | 7 + subprojects/nv-codec-headers.wrap | 6 + subprojects/openssl.wrap | 6 + subprojects/opus.wrap | 6 + .../ffmpeg-nvenc-runtime-api-version.patch | 837 +++++++++++++++++++++ subprojects/packagefiles/ffmpeg/meson.build | 35 + .../packagefiles/nv-codec-headers/meson.build | 3 + subprojects/packagefiles/openssl/meson.build | 3 + subprojects/packagefiles/opus/meson.build | 3 + subprojects/packagefiles/srt/meson.build | 3 + subprojects/packagefiles/x264/meson.build | 3 + subprojects/srt.wrap | 6 + subprojects/x264.wrap | 6 + 18 files changed, 1175 insertions(+) create mode 100755 extra/build_ffmpeg.sh create mode 100644 subprojects/ffmpeg.wrap create mode 100644 subprojects/nv-codec-headers.wrap create mode 100644 subprojects/openssl.wrap create mode 100644 subprojects/opus.wrap create mode 100644 subprojects/packagefiles/ffmpeg-nvenc-runtime-api-version.patch create mode 100644 subprojects/packagefiles/ffmpeg/meson.build create mode 100644 subprojects/packagefiles/nv-codec-headers/meson.build create mode 100644 subprojects/packagefiles/openssl/meson.build create mode 100644 subprojects/packagefiles/opus/meson.build create mode 100644 subprojects/packagefiles/srt/meson.build create mode 100644 subprojects/packagefiles/x264/meson.build create mode 100644 subprojects/srt.wrap create mode 100644 subprojects/x264.wrap diff --git a/.gitignore b/.gitignore index 89ee27d..510361f 100644 --- a/.gitignore +++ b/.gitignore @@ -30,3 +30,7 @@ gsr-kms-server *.jpg *.jpeg *.png + +subprojects/* +!subprojects/*.wrap +!subprojects/packagefiles/ diff --git a/README.md b/README.md index 26e59e5..9403e23 100644 --- a/README.md +++ b/README.md @@ -75,6 +75,14 @@ When building GPU Screen Recorder with portal support (`-Dportal=true` meson opt * libdbus * libpipewire (and libspa which is usually part of libpipewire) +## Building with a statically linked ffmpeg +The `-Dffmpeg_static=true` meson option (disabled by default) downloads ffmpeg and the libraries that ffmpeg depends on (x264, opus, openssl, srt and nv-codec-headers), builds them from source with lto and only the components that GPU Screen Recorder uses and then links them statically into GPU Screen Recorder. +This removes the runtime dependency on the system ffmpeg and on all of those libraries. `make`, `nasm`, `cmake` and `perl` are needed to build them and the first `meson setup` takes a few minutes longer because of it. + +The only libraries that ffmpeg is still dynamically linked to are libva, libdrm (which GPU Screen Recorder also uses directly) and the c/c++ runtime. + +This uses the same ffmpeg version as the GPU Screen Recorder flatpak, including the patch that makes nvenc negotiate the NVENC API version at runtime instead of at build time, which makes nvidia video encoding work on drivers down to 470.57.02 (Kepler) while still supporting AV1 and the RTX 5000 series. + ## Runtime dependencies * libglvnd (which provides libgl, libglx and libegl) is needed. Your system needs to support at least OpenGL ES 3.0 (released in 2012) * vulkan-icd-loader (which provides the runtime vulkan library. This is only needed if vulkan video encoding option is used) diff --git a/extra/build_ffmpeg.sh b/extra/build_ffmpeg.sh new file mode 100755 index 0000000..06c3344 --- /dev/null +++ b/extra/build_ffmpeg.sh @@ -0,0 +1,233 @@ +#!/bin/sh + +# Builds ffmpeg, or one of the libraries that ffmpeg depends on, from source as a +# static library with lto. Only the components that gpu-screen-recorder uses are enabled. +# This is run by meson when the "ffmpeg_static" option is enabled. + +set -eu + +if [ $# -ne 5 ]; then + echo "usage: $0 " >&2 + exit 1 +fi + +library=$1 +source_dir=$2 +build_dir=$3 +prefix=$4 +compiler=$5 +log_file=$build_dir/build.log +stamp_file=$prefix/$library.stamp +jobs=$(nproc 2>/dev/null || echo 4) + +export CC=$compiler +export PKG_CONFIG_PATH=$prefix/lib/pkgconfig${PKG_CONFIG_PATH:+:$PKG_CONFIG_PATH} + +require_program() { + if ! command -v "$1" >/dev/null 2>&1; then + echo "error: $1 is required to build $library from source" >&2 + exit 1 + fi +} + +require_program pkg-config +require_program make + +case $library in + nv-codec-headers) + set -- PREFIX="$prefix" + build() { + make -C "$source_dir" "$@" install + } + ;; + x264) + require_program nasm + set -- --prefix="$prefix" \ + --enable-static \ + --enable-pic \ + --enable-lto \ + --disable-cli \ + --disable-opencl + build() { + "$source_dir/configure" "$@" + make -j"$jobs" + make install + } + ;; + opus) + set -- --prefix="$prefix" \ + --disable-shared \ + --enable-static \ + --with-pic \ + --disable-doc \ + --disable-extra-programs \ + CFLAGS="-O3 -flto -fPIC" \ + LDFLAGS="-flto" + build() { + "$source_dir/configure" "$@" + make -j"$jobs" + make install + } + ;; + openssl) + require_program perl + # The certificate directory of the system openssl is used so that certificate + # verification keeps working the same way as it does with the system openssl. + openssl_dir=$(openssl version -d 2>/dev/null | sed -n 's/^OPENSSLDIR: "\(.*\)"$/\1/p') + if [ -z "$openssl_dir" ]; then + openssl_dir=/etc/ssl + fi + set -- --prefix="$prefix" \ + --libdir=lib \ + --openssldir="$openssl_dir" \ + no-shared \ + no-apps \ + no-docs \ + no-tests \ + no-legacy \ + no-engine \ + no-comp \ + no-quic \ + no-ssl3 \ + no-weak-ssl-ciphers \ + no-cms \ + no-ct \ + no-ts \ + no-ocsp \ + no-srp \ + no-psk \ + no-dsa \ + no-ec2m \ + no-gost \ + no-idea \ + no-md2 \ + no-md4 \ + no-mdc2 \ + no-rc2 \ + no-rc4 \ + no-rc5 \ + no-bf \ + no-cast \ + no-seed \ + no-camellia \ + no-whirlpool \ + no-rmd160 \ + no-sm2 \ + no-sm3 \ + no-sm4 \ + no-siphash \ + -O3 -flto -fPIC + build() { + "$source_dir/Configure" "$@" + make -j"$jobs" + make install_sw + } + ;; + srt) + require_program cmake + set -- -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_INSTALL_PREFIX="$prefix" \ + -DCMAKE_INSTALL_LIBDIR=lib \ + -DCMAKE_INTERPROCEDURAL_OPTIMIZATION=ON \ + -DCMAKE_POSITION_INDEPENDENT_CODE=ON \ + -DENABLE_SHARED=OFF \ + -DENABLE_STATIC=ON \ + -DENABLE_APPS=OFF \ + -DENABLE_EXAMPLES=OFF \ + -DENABLE_TESTING=OFF \ + -DENABLE_UNITTESTS=OFF \ + -DUSE_ENCLIB=openssl \ + -DOPENSSL_ROOT_DIR="$prefix" \ + -DOPENSSL_USE_STATIC_LIBS=ON + build() { + cmake -S "$source_dir" -B "$build_dir" "$@" + cmake --build "$build_dir" -j"$jobs" + cmake --install "$build_dir" + } + ;; + ffmpeg) + require_program nasm + + encoders=aac,flac,libx264,libopus,h264_nvenc,hevc_nvenc,av1_nvenc,h264_vaapi,hevc_vaapi,vp8_vaapi,vp9_vaapi,h264_vulkan,hevc_vulkan + muxers=mp4,mov,matroska,webm,flv,mpegts,hls,whip + protocols=file,pipe,tcp,udp,http,https,tls,rtmp,rtmps,libsrt + filters=abuffer,abuffersink,amix,aresample,aformat,anull + + if pkg-config --atleast-version=1.16 libva; then + encoders=$encoders,av1_vaapi + else + echo "warning: libva is too old for av1 vaapi encoding" >&2 + fi + + if pkg-config --atleast-version=1.4.317 vulkan; then + encoders=$encoders,av1_vulkan + else + echo "warning: the vulkan headers are too old for av1 vulkan encoding" >&2 + fi + + # ffmpeg ignores $CC and the same compiler as gpu-screen-recorder has to be + # used because lto objects are not compatible between compilers. + set -- --prefix="$prefix" \ + --cc="$compiler" \ + --pkg-config-flags=--static \ + --disable-debug \ + --disable-everything \ + --disable-autodetect \ + --disable-programs \ + --disable-doc \ + --disable-avdevice \ + --disable-swscale \ + --disable-shared \ + --enable-static \ + --enable-pic \ + --enable-lto \ + --enable-gpl \ + --enable-version3 \ + --enable-network \ + --enable-encoder="$encoders" \ + --enable-muxer="$muxers" \ + --enable-protocol="$protocols" \ + --enable-filter="$filters" \ + --enable-bsf=extract_extradata \ + --enable-libx264 \ + --enable-libopus \ + --enable-libsrt \ + --enable-openssl \ + --enable-ffnvcodec \ + --enable-nvenc \ + --enable-cuda \ + --enable-vaapi \ + --enable-vulkan + build() { + "$source_dir/configure" "$@" + make -j"$jobs" + make install + } + ;; + *) + echo "error: unknown library $library" >&2 + exit 1 + ;; +esac + +# Rebuilding takes a long time, only do it when the source or the configuration changed. +stamp="$source_dir $compiler $*" +if [ -f "$stamp_file" ] && [ "$(cat "$stamp_file")" = "$stamp" ]; then + exit 0 +fi + +rm -rf "$build_dir" +mkdir -p "$build_dir" + +echo "Building $library, this takes a while. Output is written to $log_file" >&2 + +( + cd "$build_dir" + build "$@" +) >"$log_file" 2>&1 || { + echo "error: failed to build $library, see $log_file for details:" >&2 + tail -n 30 "$log_file" >&2 + exit 1 +} + +printf '%s' "$stamp" > "$stamp_file" diff --git a/meson.build b/meson.build index 8e9d8a4..0f7ded6 100644 --- a/meson.build +++ b/meson.build @@ -73,6 +73,11 @@ src += protocol_src cc = meson.get_compiler('c') m_dep = cc.find_library('m', required : true) +# Overrides the libav* dependencies below with a statically linked ffmpeg built from source. +if get_option('ffmpeg_static') == true + subproject('ffmpeg') +endif + dep = [ dependency('threads'), m_dep, diff --git a/meson_options.txt b/meson_options.txt index d50e2db..23751b6 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -4,3 +4,4 @@ option('nvidia_suspend_fix', type : 'boolean', value : true, description : 'Inst option('portal', type : 'boolean', value : true, description : 'Build with support for xdg desktop portal ScreenCast capture (wayland only) (-w portal option). Requires pipewire') option('app_audio', type : 'boolean', value : true, description : 'Build with support for recording a single audio source (-a app: option). Requires pipewire') option('plugin_examples', type : 'boolean', value : false, description : 'Build plugin examples') +option('ffmpeg_static', type : 'boolean', value : false, description : 'Download and build a minimal ffmpeg and the libraries it depends on from source with lto and link them statically into gpu-screen-recorder instead of using the system ffmpeg. Requires make, nasm, cmake and perl') diff --git a/subprojects/ffmpeg.wrap b/subprojects/ffmpeg.wrap new file mode 100644 index 0000000..56e8e65 --- /dev/null +++ b/subprojects/ffmpeg.wrap @@ -0,0 +1,7 @@ +[wrap-file] +directory = ffmpeg-8.1 +source_url = https://ffmpeg.org/releases/ffmpeg-8.1.tar.xz +source_filename = ffmpeg-8.1.tar.xz +source_hash = b072aed6871998cce9b36e7774033105ca29e33632be5b6347f3206898e0756a +patch_directory = ffmpeg +diff_files = ffmpeg-nvenc-runtime-api-version.patch diff --git a/subprojects/nv-codec-headers.wrap b/subprojects/nv-codec-headers.wrap new file mode 100644 index 0000000..e3b36de --- /dev/null +++ b/subprojects/nv-codec-headers.wrap @@ -0,0 +1,6 @@ +[wrap-file] +directory = nv-codec-headers-n13.0.19.0 +source_url = https://github.com/FFmpeg/nv-codec-headers/archive/refs/tags/n13.0.19.0.tar.gz +source_filename = nv-codec-headers-13.0.19.0.tar.gz +source_hash = 86d15d1a7c0ac73a0eafdfc57bebfeba7da8264595bf531cf4d8db1c22940116 +patch_directory = nv-codec-headers diff --git a/subprojects/openssl.wrap b/subprojects/openssl.wrap new file mode 100644 index 0000000..fe386af --- /dev/null +++ b/subprojects/openssl.wrap @@ -0,0 +1,6 @@ +[wrap-file] +directory = openssl-3.6.3 +source_url = https://github.com/openssl/openssl/releases/download/openssl-3.6.3/openssl-3.6.3.tar.gz +source_filename = openssl-3.6.3.tar.gz +source_hash = 243a86649cf6f23eeb6a2ff2456e09e5d77dd9018a54d3d96b0c6bdd6ba6c7f1 +patch_directory = openssl diff --git a/subprojects/opus.wrap b/subprojects/opus.wrap new file mode 100644 index 0000000..af08d8b --- /dev/null +++ b/subprojects/opus.wrap @@ -0,0 +1,6 @@ +[wrap-file] +directory = opus-1.6.1 +source_url = https://downloads.xiph.org/releases/opus/opus-1.6.1.tar.gz +source_filename = opus-1.6.1.tar.gz +source_hash = 6ffcb593207be92584df15b32466ed64bbec99109f007c82205f0194572411a1 +patch_directory = opus diff --git a/subprojects/packagefiles/ffmpeg-nvenc-runtime-api-version.patch b/subprojects/packagefiles/ffmpeg-nvenc-runtime-api-version.patch new file mode 100644 index 0000000..53c8419 --- /dev/null +++ b/subprojects/packagefiles/ffmpeg-nvenc-runtime-api-version.patch @@ -0,0 +1,837 @@ +diff --git a/libavcodec/nvenc.c b/libavcodec/nvenc.c +index 392230526d..ba26580acb 100644 +--- a/libavcodec/nvenc.c ++++ b/libavcodec/nvenc.c +@@ -263,7 +263,15 @@ static void nvenc_map_preset(NvencContext *ctx) + + static void nvenc_print_driver_requirement(AVCodecContext *avctx, int level) + { +-#if NVENCAPI_CHECK_VERSION(13, 1) ++/* With runtime API version negotiation the build-time header version no ++ * longer dictates the required driver; the runtime floor is API 11.1. */ ++#if 1 ++# if defined(_WIN32) || defined(__CYGWIN__) ++ const char *minver = "471.41"; ++# else ++ const char *minver = "470.57.02"; ++# endif ++#elif NVENCAPI_CHECK_VERSION(13, 1) + const char *minver = "(unknown)"; + #elif NVENCAPI_CHECK_VERSION(13, 0) + const char *minver = "570.0"; +@@ -347,6 +355,193 @@ static void nvenc_print_driver_requirement(AVCodecContext *avctx, int level) + #define to_nv_color_trc(n) (uint32_t)(n) + #endif + ++/* ++ * Runtime NVENC API version negotiation, see nvenc.h. ++ * ++ * Each row holds the exact NV_ENC_*_VER constants of one SDK's nvEncodeAPI.h. ++ * A struct version word is composed as ++ * major | (minor << 24) | (struct_revision << 16) | (0x7 << 28) ++ * optionally OR'd with (1u << 31); the driver validates the whole word, so ++ * presenting a row of these values is indistinguishable from an FFmpeg that ++ * was actually compiled against that SDK's headers. ++ */ ++#define NVENC_API_WORD(major, minor) \ ++ ((uint32_t)(major) | ((uint32_t)(minor) << 24)) ++#define NVENC_STRUCT_VER(major, minor, rev) \ ++ (NVENC_API_WORD(major, minor) | ((uint32_t)(rev) << 16) | (0x7u << 28)) ++#define NVENC_STRUCT_VER_RES(major, minor, rev) \ ++ (NVENC_STRUCT_VER(major, minor, rev) | (1u << 31)) ++ ++// Newest first; negotiation picks the first row the driver supports. ++static const NvencApiVersion nvenc_api_versions[] = { ++ { // SDK 13.0, driver >= 570.0 (Linux) / 570.0 (Windows) ++ .packed = NVENC_API_PACKED(13, 0), ++ .api = NVENC_API_WORD(13, 0), ++ .function_list = NVENC_STRUCT_VER(13, 0, 2), ++ .open_session_params = NVENC_STRUCT_VER(13, 0, 1), ++ .caps_param = NVENC_STRUCT_VER(13, 0, 1), ++ .initialize_params = NVENC_STRUCT_VER_RES(13, 0, 7), ++ .config = NVENC_STRUCT_VER_RES(13, 0, 9), ++ .preset_config = NVENC_STRUCT_VER_RES(13, 0, 5), ++ .pic_params = NVENC_STRUCT_VER_RES(13, 0, 7), ++ .lock_bitstream = NVENC_STRUCT_VER_RES(13, 0, 2), ++ .lock_input_buffer = NVENC_STRUCT_VER(13, 0, 1), ++ .map_input_resource = NVENC_STRUCT_VER(13, 0, 4), ++ .register_resource = NVENC_STRUCT_VER(13, 0, 5), ++ .create_input_buffer = NVENC_STRUCT_VER(13, 0, 2), ++ .create_bitstream_buffer = NVENC_STRUCT_VER(13, 0, 1), ++ .reconfigure_params = NVENC_STRUCT_VER_RES(13, 0, 2), ++ .sequence_param_payload = NVENC_STRUCT_VER(13, 0, 1), ++ }, ++ { // SDK 12.2, driver >= 550.54.14 (Linux) / 551.76 (Windows) ++ .packed = NVENC_API_PACKED(12, 2), ++ .api = NVENC_API_WORD(12, 2), ++ .function_list = NVENC_STRUCT_VER(12, 2, 2), ++ .open_session_params = NVENC_STRUCT_VER(12, 2, 1), ++ .caps_param = NVENC_STRUCT_VER(12, 2, 1), ++ .initialize_params = NVENC_STRUCT_VER_RES(12, 2, 7), ++ .config = NVENC_STRUCT_VER_RES(12, 2, 9), ++ .preset_config = NVENC_STRUCT_VER_RES(12, 2, 5), ++ .pic_params = NVENC_STRUCT_VER_RES(12, 2, 7), ++ .lock_bitstream = NVENC_STRUCT_VER_RES(12, 2, 2), ++ .lock_input_buffer = NVENC_STRUCT_VER(12, 2, 1), ++ .map_input_resource = NVENC_STRUCT_VER(12, 2, 4), ++ .register_resource = NVENC_STRUCT_VER(12, 2, 5), ++ .create_input_buffer = NVENC_STRUCT_VER(12, 2, 2), ++ .create_bitstream_buffer = NVENC_STRUCT_VER(12, 2, 1), ++ .reconfigure_params = NVENC_STRUCT_VER_RES(12, 2, 2), ++ .sequence_param_payload = NVENC_STRUCT_VER(12, 2, 1), ++ }, ++ { // SDK 12.1, driver >= 530.41.03 (Linux) / 531.61 (Windows) ++ .packed = NVENC_API_PACKED(12, 1), ++ .api = NVENC_API_WORD(12, 1), ++ .function_list = NVENC_STRUCT_VER(12, 1, 2), ++ .open_session_params = NVENC_STRUCT_VER(12, 1, 1), ++ .caps_param = NVENC_STRUCT_VER(12, 1, 1), ++ .initialize_params = NVENC_STRUCT_VER_RES(12, 1, 6), ++ .config = NVENC_STRUCT_VER_RES(12, 1, 8), ++ .preset_config = NVENC_STRUCT_VER_RES(12, 1, 4), ++ .pic_params = NVENC_STRUCT_VER_RES(12, 1, 6), ++ .lock_bitstream = NVENC_STRUCT_VER_RES(12, 1, 1), ++ .lock_input_buffer = NVENC_STRUCT_VER(12, 1, 1), ++ .map_input_resource = NVENC_STRUCT_VER(12, 1, 4), ++ .register_resource = NVENC_STRUCT_VER(12, 1, 4), ++ .create_input_buffer = NVENC_STRUCT_VER(12, 1, 1), ++ .create_bitstream_buffer = NVENC_STRUCT_VER(12, 1, 1), ++ .reconfigure_params = NVENC_STRUCT_VER_RES(12, 1, 1), ++ .sequence_param_payload = NVENC_STRUCT_VER(12, 1, 1), ++ }, ++ { // SDK 12.0, driver >= 520.56.06 (Linux) / 522.25 (Windows) ++ .packed = NVENC_API_PACKED(12, 0), ++ .api = NVENC_API_WORD(12, 0), ++ .function_list = NVENC_STRUCT_VER(12, 0, 2), ++ .open_session_params = NVENC_STRUCT_VER(12, 0, 1), ++ .caps_param = NVENC_STRUCT_VER(12, 0, 1), ++ .initialize_params = NVENC_STRUCT_VER_RES(12, 0, 5), ++ .config = NVENC_STRUCT_VER_RES(12, 0, 8), ++ .preset_config = NVENC_STRUCT_VER_RES(12, 0, 4), ++ .pic_params = NVENC_STRUCT_VER_RES(12, 0, 6), ++ .lock_bitstream = NVENC_STRUCT_VER(12, 0, 2), ++ .lock_input_buffer = NVENC_STRUCT_VER(12, 0, 1), ++ .map_input_resource = NVENC_STRUCT_VER(12, 0, 4), ++ .register_resource = NVENC_STRUCT_VER(12, 0, 4), ++ .create_input_buffer = NVENC_STRUCT_VER(12, 0, 1), ++ .create_bitstream_buffer = NVENC_STRUCT_VER(12, 0, 1), ++ .reconfigure_params = NVENC_STRUCT_VER_RES(12, 0, 1), ++ .sequence_param_payload = NVENC_STRUCT_VER(12, 0, 1), ++ }, ++ { // SDK 11.1, driver >= 470.57.02 (Linux) / 471.41 (Windows) - Kepler's last branch ++ .packed = NVENC_API_PACKED(11, 1), ++ .api = NVENC_API_WORD(11, 1), ++ .function_list = NVENC_STRUCT_VER(11, 1, 2), ++ .open_session_params = NVENC_STRUCT_VER(11, 1, 1), ++ .caps_param = NVENC_STRUCT_VER(11, 1, 1), ++ .initialize_params = NVENC_STRUCT_VER_RES(11, 1, 5), ++ .config = NVENC_STRUCT_VER_RES(11, 1, 7), ++ .preset_config = NVENC_STRUCT_VER_RES(11, 1, 4), ++ .pic_params = NVENC_STRUCT_VER_RES(11, 1, 4), ++ .lock_bitstream = NVENC_STRUCT_VER(11, 1, 1), ++ .lock_input_buffer = NVENC_STRUCT_VER(11, 1, 1), ++ .map_input_resource = NVENC_STRUCT_VER(11, 1, 4), ++ .register_resource = NVENC_STRUCT_VER(11, 1, 3), ++ .create_input_buffer = NVENC_STRUCT_VER(11, 1, 1), ++ .create_bitstream_buffer = NVENC_STRUCT_VER(11, 1, 1), ++ .reconfigure_params = NVENC_STRUCT_VER_RES(11, 1, 1), ++ .sequence_param_payload = NVENC_STRUCT_VER(11, 1, 1), ++ }, ++}; ++ ++/* The newest row must be exactly what this build would present anyway; if any ++ * of these fire, the headers changed and the whole table needs re-validation. */ ++_Static_assert(NVENC_API_WORD(13, 0) == NVENCAPI_VERSION, "nvenc api version table outdated"); ++_Static_assert(NVENC_STRUCT_VER(13, 0, 2) == NV_ENCODE_API_FUNCTION_LIST_VER, "nvenc api version table outdated"); ++_Static_assert(NVENC_STRUCT_VER(13, 0, 1) == NV_ENC_OPEN_ENCODE_SESSION_EX_PARAMS_VER, "nvenc api version table outdated"); ++_Static_assert(NVENC_STRUCT_VER(13, 0, 1) == NV_ENC_CAPS_PARAM_VER, "nvenc api version table outdated"); ++_Static_assert(NVENC_STRUCT_VER_RES(13, 0, 7) == NV_ENC_INITIALIZE_PARAMS_VER, "nvenc api version table outdated"); ++_Static_assert(NVENC_STRUCT_VER_RES(13, 0, 9) == NV_ENC_CONFIG_VER, "nvenc api version table outdated"); ++_Static_assert(NVENC_STRUCT_VER_RES(13, 0, 5) == NV_ENC_PRESET_CONFIG_VER, "nvenc api version table outdated"); ++_Static_assert(NVENC_STRUCT_VER_RES(13, 0, 7) == NV_ENC_PIC_PARAMS_VER, "nvenc api version table outdated"); ++_Static_assert(NVENC_STRUCT_VER_RES(13, 0, 2) == NV_ENC_LOCK_BITSTREAM_VER, "nvenc api version table outdated"); ++_Static_assert(NVENC_STRUCT_VER(13, 0, 1) == NV_ENC_LOCK_INPUT_BUFFER_VER, "nvenc api version table outdated"); ++_Static_assert(NVENC_STRUCT_VER(13, 0, 4) == NV_ENC_MAP_INPUT_RESOURCE_VER, "nvenc api version table outdated"); ++_Static_assert(NVENC_STRUCT_VER(13, 0, 5) == NV_ENC_REGISTER_RESOURCE_VER, "nvenc api version table outdated"); ++_Static_assert(NVENC_STRUCT_VER(13, 0, 2) == NV_ENC_CREATE_INPUT_BUFFER_VER, "nvenc api version table outdated"); ++_Static_assert(NVENC_STRUCT_VER(13, 0, 1) == NV_ENC_CREATE_BITSTREAM_BUFFER_VER, "nvenc api version table outdated"); ++_Static_assert(NVENC_STRUCT_VER_RES(13, 0, 2) == NV_ENC_RECONFIGURE_PARAMS_VER, "nvenc api version table outdated"); ++_Static_assert(NVENC_STRUCT_VER(13, 0, 1) == NV_ENC_SEQUENCE_PARAM_PAYLOAD_VER, "nvenc api version table outdated"); ++ ++/* Layout guards for the compatibility padding after NV_ENC_INITIALIZE_PARAMS / ++ * NV_ENC_RECONFIGURE_PARAMS / NV_ENC_LOCK_BITSTREAM (their pre-12.2 layouts ++ * are up to 8 bytes larger: 1808, 1824 and 1552 bytes respectively) and for ++ * the legacy bit-depth pokes below. */ ++_Static_assert(sizeof(NV_ENC_INITIALIZE_PARAMS) == 1800, "unexpected NV_ENC_INITIALIZE_PARAMS layout"); ++_Static_assert(sizeof(NV_ENC_RECONFIGURE_PARAMS) == 1816, "unexpected NV_ENC_RECONFIGURE_PARAMS layout"); ++_Static_assert(sizeof(NV_ENC_LOCK_BITSTREAM) == 1544, "unexpected NV_ENC_LOCK_BITSTREAM layout"); ++_Static_assert(offsetof(NV_ENC_CONFIG_HEVC, tier) == 4 && offsetof(NV_ENC_CONFIG_HEVC, idrPeriod) == 20, ++ "unexpected NV_ENC_CONFIG_HEVC layout"); ++_Static_assert(offsetof(NV_ENC_CONFIG_AV1, tier) == 4 && offsetof(NV_ENC_CONFIG_AV1, idrPeriod) == 20, ++ "unexpected NV_ENC_CONFIG_AV1 layout"); ++ ++/* ++ * SDK 12.2 removed the NV_ENC_CONFIG_HEVC.pixelBitDepthMinus8 and ++ * NV_ENC_CONFIG_AV1.inputPixelBitDepthMinus8/pixelBitDepthMinus8 bitfields ++ * and replaced them with the inputBitDepth/outputBitDepth enums that live at ++ * different offsets. Drivers negotiated to API < 12.2 still read the old bit ++ * positions (reserved-and-zero in the compiled 12.2+ layout), so 10 bit ++ * encoding on those drivers has to set the old bits by hand. ++ * ++ * Verified against the SDK 11.1/12.0/12.1 headers: both codec configs keep ++ * their flag bitfields in the uint32 word at byte offset 16 (between ++ * maxCUSize/maxPartSize at 12 and idrPeriod at 20, anchored by the asserts ++ * above). Within that word: ++ * NV_ENC_CONFIG_HEVC: pixelBitDepthMinus8 = bits 11-13 ++ * NV_ENC_CONFIG_AV1: inputPixelBitDepthMinus8 = bits 12-14, ++ * pixelBitDepthMinus8 = bits 15-17 ++ */ ++static void nvenc_legacy_bitfield_poke(void *codec_config, size_t word_offset, ++ unsigned bit_offset, unsigned width, uint32_t value) ++{ ++ uint32_t word; ++ memcpy(&word, (uint8_t*)codec_config + word_offset, sizeof(word)); ++ word &= ~(((1u << width) - 1) << bit_offset); ++ word |= (value & ((1u << width) - 1)) << bit_offset; ++ memcpy((uint8_t*)codec_config + word_offset, &word, sizeof(word)); ++} ++ ++static void nvenc_set_legacy_hevc_bit_depth(NV_ENC_CONFIG_HEVC *hevc, uint32_t depth_minus8) ++{ ++ nvenc_legacy_bitfield_poke(hevc, 16, 11, 3, depth_minus8); ++} ++ ++#if CONFIG_AV1_NVENC_ENCODER ++static void nvenc_set_legacy_av1_bit_depth(NV_ENC_CONFIG_AV1 *av1, ++ uint32_t input_depth_minus8, uint32_t output_depth_minus8) ++{ ++ nvenc_legacy_bitfield_poke(av1, 16, 12, 3, input_depth_minus8); ++ nvenc_legacy_bitfield_poke(av1, 16, 15, 3, output_depth_minus8); ++} ++#endif ++ + static av_cold int nvenc_load_libraries(AVCodecContext *avctx) + { + NvencContext *ctx = avctx->priv_data; +@@ -371,16 +566,35 @@ static av_cold int nvenc_load_libraries(AVCodecContext *avctx) + + av_log(avctx, AV_LOG_VERBOSE, "Loaded Nvenc version %d.%d\n", nvenc_max_ver >> 4, nvenc_max_ver & 0xf); + +- if ((NVENCAPI_MAJOR_VERSION << 4 | NVENCAPI_MINOR_VERSION) > nvenc_max_ver) { ++ /* Negotiate: present the newest API version both the driver and this ++ * build support instead of failing on drivers older than the headers. */ ++ ctx->api_vers = NULL; ++ for (int i = 0; i < (int)FF_ARRAY_ELEMS(nvenc_api_versions); i++) { ++ if (nvenc_api_versions[i].packed <= nvenc_max_ver) { ++ ctx->api_vers = &nvenc_api_versions[i]; ++ break; ++ } ++ } ++ ++ if (!ctx->api_vers) { + av_log(avctx, AV_LOG_ERROR, "Driver does not support the required nvenc API version. " + "Required: %d.%d Found: %d.%d\n", +- NVENCAPI_MAJOR_VERSION, NVENCAPI_MINOR_VERSION, ++ 11, 1, + nvenc_max_ver >> 4, nvenc_max_ver & 0xf); + nvenc_print_driver_requirement(avctx, AV_LOG_ERROR); + return AVERROR(ENOSYS); + } + +- dl_fn->nvenc_funcs.version = NV_ENCODE_API_FUNCTION_LIST_VER; ++ ctx->api_version = ctx->api_vers->packed; ++ ++ if (ctx->api_version < NVENC_API_PACKED(NVENCAPI_MAJOR_VERSION, NVENCAPI_MINOR_VERSION)) ++ av_log(avctx, AV_LOG_VERBOSE, "Driver supports nvenc API up to %d.%d (compiled for %d.%d), " ++ "using API %d.%d; features requiring a newer driver are disabled\n", ++ nvenc_max_ver >> 4, nvenc_max_ver & 0xf, ++ NVENCAPI_MAJOR_VERSION, NVENCAPI_MINOR_VERSION, ++ ctx->api_version >> 4, ctx->api_version & 0xf); ++ ++ dl_fn->nvenc_funcs.version = ctx->api_vers->function_list; + + err = dl_fn->nvenc_dl->NvEncodeAPICreateInstance(&dl_fn->nvenc_funcs); + if (err != NV_ENC_SUCCESS) +@@ -421,8 +635,8 @@ static av_cold int nvenc_open_session(AVCodecContext *avctx) + NV_ENCODE_API_FUNCTION_LIST *p_nvenc = &ctx->nvenc_dload_funcs.nvenc_funcs; + NVENCSTATUS ret; + +- params.version = NV_ENC_OPEN_ENCODE_SESSION_EX_PARAMS_VER; +- params.apiVersion = NVENCAPI_VERSION; ++ params.version = ctx->api_vers->open_session_params; ++ params.apiVersion = ctx->api_vers->api; + if (ctx->d3d11_device) { + params.device = ctx->d3d11_device; + params.deviceType = NV_ENC_DEVICE_TYPE_DIRECTX; +@@ -483,7 +697,7 @@ static int nvenc_check_cap(AVCodecContext *avctx, NV_ENC_CAPS cap) + NV_ENC_CAPS_PARAM params = { 0 }; + int ret, val = 0; + +- params.version = NV_ENC_CAPS_PARAM_VER; ++ params.version = ctx->api_vers->caps_param; + params.capsToQuery = cap; + + ret = p_nvenc->nvEncGetEncodeCaps(ctx->nvencoder, ctx->init_encode_params.encodeGUID, ¶ms, &val); +@@ -498,6 +712,15 @@ static int nvenc_check_capabilities(AVCodecContext *avctx) + NvencContext *ctx = avctx->priv_data; + int tmp, ret; + ++#if CONFIG_AV1_NVENC_ENCODER ++ if (avctx->codec->id == AV_CODEC_ID_AV1 && !NVENC_API_AT_LEAST(ctx, 12, 0)) { ++ av_log(avctx, AV_LOG_ERROR, "AV1 encoding needs nvenc API 12.0, but the driver " ++ "only supports %d.%d. Driver 520.56.06 or newer is required for AV1.\n", ++ ctx->api_version >> 4, ctx->api_version & 0xf); ++ return AVERROR(ENOSYS); ++ } ++#endif ++ + ret = nvenc_check_codec_support(avctx); + if (ret < 0) { + av_log(avctx, AV_LOG_WARNING, "Codec not supported\n"); +@@ -1219,7 +1442,10 @@ static av_cold int nvenc_setup_rate_control(AVCodecContext *avctx) + ctx->encode_config.rcParams.lookaheadDepth, ctx->rc_lookahead); + + #ifdef NVENC_HAVE_LOOKAHEAD_LEVEL +- if (ctx->lookahead_level >= 0) { ++ if (ctx->lookahead_level >= 0 && !NVENC_API_AT_LEAST(ctx, 12, 2)) { ++ av_log(avctx, AV_LOG_WARNING, "Lookahead level needs nvenc API 12.2 " ++ "(driver 550.54.14+), ignoring.\n"); ++ } else if (ctx->lookahead_level >= 0) { + switch (ctx->lookahead_level) { + case NV_ENC_LOOKAHEAD_LEVEL_0: + case NV_ENC_LOOKAHEAD_LEVEL_1: +@@ -1413,8 +1639,12 @@ static av_cold int nvenc_setup_h264_config(AVCodecContext *avctx) + h264->level = ctx->level; + + #ifdef NVENC_HAVE_NEW_BIT_DEPTH_API +- h264->inputBitDepth = IS_10BIT(ctx->data_pix_fmt) ? NV_ENC_BIT_DEPTH_10 : NV_ENC_BIT_DEPTH_8; +- h264->outputBitDepth = (IS_10BIT(ctx->data_pix_fmt) || ctx->highbitdepth) ? NV_ENC_BIT_DEPTH_10 : NV_ENC_BIT_DEPTH_8; ++ /* Pre-12.2 drivers read reserved space at these offsets; H264 on such ++ * drivers is always 8 bit (10 bit was rejected by the caps check). */ ++ if (NVENC_API_AT_LEAST(ctx, 12, 2)) { ++ h264->inputBitDepth = IS_10BIT(ctx->data_pix_fmt) ? NV_ENC_BIT_DEPTH_10 : NV_ENC_BIT_DEPTH_8; ++ h264->outputBitDepth = (IS_10BIT(ctx->data_pix_fmt) || ctx->highbitdepth) ? NV_ENC_BIT_DEPTH_10 : NV_ENC_BIT_DEPTH_8; ++ } + #endif + + if (ctx->coder >= 0) +@@ -1450,6 +1680,13 @@ static av_cold int nvenc_setup_h264_config(AVCodecContext *avctx) + #endif + + #ifdef NVENC_HAVE_TIME_CODE ++ /* On by default; quietly drop it on old drivers like a pre-12.2 build ++ * (this also disables the per-frame timeCode fill). */ ++ if (ctx->s12m_tc && !NVENC_API_AT_LEAST(ctx, 12, 2)) { ++ av_log(avctx, AV_LOG_VERBOSE, "S12M timecodes need nvenc API 12.2, disabling.\n"); ++ ctx->s12m_tc = 0; ++ } ++ + if (ctx->s12m_tc) + h264->enableTimeCode = 1; + #endif +@@ -1501,7 +1738,8 @@ static av_cold int nvenc_setup_hevc_config(AVCodecContext *avctx) + hevc->intraRefreshCnt = cc->gopLength - 1; + cc->gopLength = NVENC_INFINITE_GOPLENGTH; + #ifdef NVENC_HAVE_HEVC_OUTPUT_RECOVERY_POINT_SEI +- hevc->outputRecoveryPointSEI = 1; ++ if (NVENC_API_AT_LEAST(ctx, 12, 0)) ++ hevc->outputRecoveryPointSEI = 1; + #endif + #ifdef NVENC_HAVE_SINGLE_SLICE_INTRA_REFRESH + hevc->singleSliceIntraRefresh = ctx->single_slice_intra_refresh; +@@ -1509,12 +1747,14 @@ static av_cold int nvenc_setup_hevc_config(AVCodecContext *avctx) + } + + #ifdef NVENC_HAVE_HEVC_AND_AV1_MASTERING_METADATA +- ctx->mdm = hevc->outputMasteringDisplay = !!av_frame_side_data_get(avctx->decoded_side_data, +- avctx->nb_decoded_side_data, +- AV_FRAME_DATA_MASTERING_DISPLAY_METADATA); +- ctx->cll = hevc->outputMaxCll = !!av_frame_side_data_get(avctx->decoded_side_data, +- avctx->nb_decoded_side_data, +- AV_FRAME_DATA_CONTENT_LIGHT_LEVEL); ++ if (NVENC_API_AT_LEAST(ctx, 13, 0)) { ++ ctx->mdm = hevc->outputMasteringDisplay = !!av_frame_side_data_get(avctx->decoded_side_data, ++ avctx->nb_decoded_side_data, ++ AV_FRAME_DATA_MASTERING_DISPLAY_METADATA); ++ ctx->cll = hevc->outputMaxCll = !!av_frame_side_data_get(avctx->decoded_side_data, ++ avctx->nb_decoded_side_data, ++ AV_FRAME_DATA_CONTENT_LIGHT_LEVEL); ++ } + #endif + + #ifdef NVENC_HAVE_HEVC_CONSTRAINED_ENCODING +@@ -1613,8 +1853,18 @@ static av_cold int nvenc_setup_hevc_config(AVCodecContext *avctx) + hevc->chromaFormatIDC = IS_YUV444(ctx->data_pix_fmt) ? 3 : IS_YUV422(ctx->data_pix_fmt) ? 2 : 1; + + #ifdef NVENC_HAVE_NEW_BIT_DEPTH_API +- hevc->inputBitDepth = IS_10BIT(ctx->data_pix_fmt) ? NV_ENC_BIT_DEPTH_10 : NV_ENC_BIT_DEPTH_8; +- hevc->outputBitDepth = (IS_10BIT(ctx->data_pix_fmt) || ctx->highbitdepth) ? NV_ENC_BIT_DEPTH_10 : NV_ENC_BIT_DEPTH_8; ++ if (NVENC_API_AT_LEAST(ctx, 12, 2)) { ++ hevc->inputBitDepth = IS_10BIT(ctx->data_pix_fmt) ? NV_ENC_BIT_DEPTH_10 : NV_ENC_BIT_DEPTH_8; ++ hevc->outputBitDepth = (IS_10BIT(ctx->data_pix_fmt) || ctx->highbitdepth) ? NV_ENC_BIT_DEPTH_10 : NV_ENC_BIT_DEPTH_8; ++ } else { ++ if (ctx->highbitdepth && !IS_10BIT(ctx->data_pix_fmt)) { ++ av_log(avctx, AV_LOG_ERROR, "8 -> 10 bit HEVC encoding needs nvenc API 12.2 " ++ "(driver 550.54.14+).\n"); ++ return AVERROR(ENOSYS); ++ } ++ // The old pixelBitDepthMinus8 bitfield read by pre-12.2 drivers ++ nvenc_set_legacy_hevc_bit_depth(hevc, IS_10BIT(ctx->data_pix_fmt) ? 2 : 0); ++ } + #else + hevc->pixelBitDepthMinus8 = IS_10BIT(ctx->data_pix_fmt) ? 2 : 0; + #endif +@@ -1722,20 +1972,28 @@ static av_cold int nvenc_setup_av1_config(AVCodecContext *avctx) + av1->chromaFormatIDC = IS_YUV444(ctx->data_pix_fmt) ? 3 : 1; + + #ifdef NVENC_HAVE_NEW_BIT_DEPTH_API +- av1->inputBitDepth = IS_10BIT(ctx->data_pix_fmt) ? NV_ENC_BIT_DEPTH_10 : NV_ENC_BIT_DEPTH_8; +- av1->outputBitDepth = (IS_10BIT(ctx->data_pix_fmt) || ctx->highbitdepth) ? NV_ENC_BIT_DEPTH_10 : NV_ENC_BIT_DEPTH_8; ++ if (NVENC_API_AT_LEAST(ctx, 12, 2)) { ++ av1->inputBitDepth = IS_10BIT(ctx->data_pix_fmt) ? NV_ENC_BIT_DEPTH_10 : NV_ENC_BIT_DEPTH_8; ++ av1->outputBitDepth = (IS_10BIT(ctx->data_pix_fmt) || ctx->highbitdepth) ? NV_ENC_BIT_DEPTH_10 : NV_ENC_BIT_DEPTH_8; ++ } else { ++ // The old bitfields read by 12.0/12.1 drivers ++ nvenc_set_legacy_av1_bit_depth(av1, IS_10BIT(ctx->data_pix_fmt) ? 2 : 0, ++ (IS_10BIT(ctx->data_pix_fmt) || ctx->highbitdepth) ? 2 : 0); ++ } + #else + av1->inputPixelBitDepthMinus8 = IS_10BIT(ctx->data_pix_fmt) ? 2 : 0; + av1->pixelBitDepthMinus8 = (IS_10BIT(ctx->data_pix_fmt) || ctx->highbitdepth) ? 2 : 0; + #endif + + #ifdef NVENC_HAVE_HEVC_AND_AV1_MASTERING_METADATA +- ctx->mdm = av1->outputMasteringDisplay = !!av_frame_side_data_get(avctx->decoded_side_data, +- avctx->nb_decoded_side_data, +- AV_FRAME_DATA_MASTERING_DISPLAY_METADATA); +- ctx->cll = av1->outputMaxCll = !!av_frame_side_data_get(avctx->decoded_side_data, +- avctx->nb_decoded_side_data, +- AV_FRAME_DATA_CONTENT_LIGHT_LEVEL); ++ if (NVENC_API_AT_LEAST(ctx, 13, 0)) { ++ ctx->mdm = av1->outputMasteringDisplay = !!av_frame_side_data_get(avctx->decoded_side_data, ++ avctx->nb_decoded_side_data, ++ AV_FRAME_DATA_MASTERING_DISPLAY_METADATA); ++ ctx->cll = av1->outputMaxCll = !!av_frame_side_data_get(avctx->decoded_side_data, ++ avctx->nb_decoded_side_data, ++ AV_FRAME_DATA_CONTENT_LIGHT_LEVEL); ++ } + #endif + + if (ctx->b_ref_mode >= 0) +@@ -1829,16 +2087,16 @@ static av_cold int nvenc_setup_encoder(AVCodecContext *avctx) + int res = 0; + int dw, dh; + +- ctx->encode_config.version = NV_ENC_CONFIG_VER; +- ctx->init_encode_params.version = NV_ENC_INITIALIZE_PARAMS_VER; ++ ctx->encode_config.version = ctx->api_vers->config; ++ ctx->init_encode_params.version = ctx->api_vers->initialize_params; + + ctx->init_encode_params.encodeHeight = avctx->height; + ctx->init_encode_params.encodeWidth = avctx->width; + + ctx->init_encode_params.encodeConfig = &ctx->encode_config; + +- preset_config.version = NV_ENC_PRESET_CONFIG_VER; +- preset_config.presetCfg.version = NV_ENC_CONFIG_VER; ++ preset_config.version = ctx->api_vers->preset_config; ++ preset_config.presetCfg.version = ctx->api_vers->config; + + #ifdef NVENC_HAVE_NEW_PRESETS + ctx->init_encode_params.tuningInfo = ctx->tuning_info; +@@ -1848,6 +2106,16 @@ static av_cold int nvenc_setup_encoder(AVCodecContext *avctx) + else if (ctx->flags & NVENC_LOWLATENCY) + ctx->init_encode_params.tuningInfo = NV_ENC_TUNING_INFO_LOW_LATENCY; + ++#ifdef NVENC_HAVE_UHQ_TUNING ++ if (ctx->init_encode_params.tuningInfo == NV_ENC_TUNING_INFO_ULTRA_HIGH_QUALITY && ++ (!NVENC_API_AT_LEAST(ctx, 12, 2) || ++ (avctx->codec->id == AV_CODEC_ID_AV1 && !NVENC_API_AT_LEAST(ctx, 13, 0)))) { ++ av_log(avctx, AV_LOG_ERROR, "UHQ tuning needs nvenc API 12.2 (driver 550.54.14+), " ++ "13.0 (driver 570+) for AV1.\n"); ++ return AVERROR(ENOSYS); ++ } ++#endif ++ + nv_status = p_nvenc->nvEncGetEncodePresetConfigEx(ctx->nvencoder, + ctx->init_encode_params.encodeGUID, + ctx->init_encode_params.presetGUID, +@@ -1864,7 +2132,7 @@ static av_cold int nvenc_setup_encoder(AVCodecContext *avctx) + + memcpy(&ctx->encode_config, &preset_config.presetCfg, sizeof(ctx->encode_config)); + +- ctx->encode_config.version = NV_ENC_CONFIG_VER; ++ ctx->encode_config.version = ctx->api_vers->config; + + compute_dar(avctx, &dw, &dh); + ctx->init_encode_params.darHeight = dh; +@@ -1897,11 +2165,16 @@ static av_cold int nvenc_setup_encoder(AVCodecContext *avctx) + ctx->init_encode_params.enableWeightedPrediction = 1; + + #ifdef NVENC_HAVE_SPLIT_FRAME_ENCODING +- ctx->init_encode_params.splitEncodeMode = ctx->split_encode_mode; ++ if (NVENC_API_AT_LEAST(ctx, 12, 1)) { ++ ctx->init_encode_params.splitEncodeMode = ctx->split_encode_mode; + +- if (ctx->split_encode_mode != NV_ENC_SPLIT_DISABLE_MODE) { +- if (avctx->codec->id == AV_CODEC_ID_HEVC && ctx->weighted_pred == 1) +- av_log(avctx, AV_LOG_WARNING, "Split encoding not supported with weighted prediction enabled.\n"); ++ if (ctx->split_encode_mode != NV_ENC_SPLIT_DISABLE_MODE) { ++ if (avctx->codec->id == AV_CODEC_ID_HEVC && ctx->weighted_pred == 1) ++ av_log(avctx, AV_LOG_WARNING, "Split encoding not supported with weighted prediction enabled.\n"); ++ } ++ } else if (ctx->split_encode_mode != NV_ENC_SPLIT_AUTO_MODE) { ++ av_log(avctx, AV_LOG_WARNING, "Split frame encoding needs nvenc API 12.1 " ++ "(driver 530.41.03+), ignoring.\n"); + } + #endif + +@@ -2042,7 +2315,7 @@ static av_cold int nvenc_alloc_surface(AVCodecContext *avctx, int idx) + + NVENCSTATUS nv_status; + NV_ENC_CREATE_BITSTREAM_BUFFER allocOut = { 0 }; +- allocOut.version = NV_ENC_CREATE_BITSTREAM_BUFFER_VER; ++ allocOut.version = ctx->api_vers->create_bitstream_buffer; + + if (avctx->pix_fmt == AV_PIX_FMT_CUDA || avctx->pix_fmt == AV_PIX_FMT_D3D11) { + ctx->surfaces[idx].in_ref = av_frame_alloc(); +@@ -2058,7 +2331,7 @@ static av_cold int nvenc_alloc_surface(AVCodecContext *avctx, int idx) + return AVERROR(EINVAL); + } + +- allocSurf.version = NV_ENC_CREATE_INPUT_BUFFER_VER; ++ allocSurf.version = ctx->api_vers->create_input_buffer; + allocSurf.width = avctx->width; + allocSurf.height = avctx->height; + allocSurf.bufferFmt = ctx->surfaces[idx].format; +@@ -2146,7 +2419,7 @@ static av_cold int nvenc_setup_extradata(AVCodecContext *avctx) + char tmpHeader[NV_MAX_SEQ_HDR_LEN]; + + NV_ENC_SEQUENCE_PARAM_PAYLOAD payload = { 0 }; +- payload.version = NV_ENC_SEQUENCE_PARAM_PAYLOAD_VER; ++ payload.version = ctx->api_vers->sequence_param_payload; + + payload.spsppsBuffer = tmpHeader; + payload.inBufferSize = sizeof(tmpHeader); +@@ -2178,7 +2451,7 @@ av_cold int ff_nvenc_encode_close(AVCodecContext *avctx) + + /* the encoder has to be flushed before it can be closed */ + if (ctx->nvencoder) { +- NV_ENC_PIC_PARAMS params = { .version = NV_ENC_PIC_PARAMS_VER, ++ NV_ENC_PIC_PARAMS params = { .version = ctx->api_vers->pic_params, + .encodePicFlags = NV_ENC_PIC_FLAG_EOS }; + + res = nvenc_push_context(avctx); +@@ -2402,7 +2675,7 @@ static int nvenc_register_frame(AVCodecContext *avctx, const AVFrame *frame) + if (idx < 0) + return idx; + +- reg.version = NV_ENC_REGISTER_RESOURCE_VER; ++ reg.version = ctx->api_vers->register_resource; + reg.width = frames_ctx->width; + reg.height = frames_ctx->height; + reg.pitch = frame->linesize[0]; +@@ -2457,7 +2730,7 @@ static int nvenc_upload_frame(AVCodecContext *avctx, const AVFrame *frame, + return res; + + if (!ctx->registered_frames[reg_idx].mapped) { +- ctx->registered_frames[reg_idx].in_map.version = NV_ENC_MAP_INPUT_RESOURCE_VER; ++ ctx->registered_frames[reg_idx].in_map.version = ctx->api_vers->map_input_resource; + ctx->registered_frames[reg_idx].in_map.registeredResource = ctx->registered_frames[reg_idx].regptr; + nv_status = p_nvenc->nvEncMapInputResource(ctx->nvencoder, &ctx->registered_frames[reg_idx].in_map); + if (nv_status != NV_ENC_SUCCESS) { +@@ -2477,7 +2750,7 @@ static int nvenc_upload_frame(AVCodecContext *avctx, const AVFrame *frame, + } else { + NV_ENC_LOCK_INPUT_BUFFER lockBufferParams = { 0 }; + +- lockBufferParams.version = NV_ENC_LOCK_INPUT_BUFFER_VER; ++ lockBufferParams.version = ctx->api_vers->lock_input_buffer; + lockBufferParams.inputBuffer = nvenc_frame->input_surface; + + nv_status = p_nvenc->nvEncLockInputBuffer(ctx->nvencoder, &lockBufferParams); +@@ -2725,31 +2998,38 @@ static int process_output_surface(AVCodecContext *avctx, AVPacket *pkt, NvencSur + NvencDynLoadFunctions *dl_fn = &ctx->nvenc_dload_funcs; + NV_ENCODE_API_FUNCTION_LIST *p_nvenc = &dl_fn->nvenc_funcs; + +- NV_ENC_LOCK_BITSTREAM lock_params = { 0 }; ++ /* The SDK 12.1 NV_ENC_LOCK_BITSTREAM layout is 8 bytes larger than the ++ * 12.2+ layout this is compiled against; keep zeroed spare space after it ++ * for drivers negotiated to API <= 12.1. */ ++ struct { ++ NV_ENC_LOCK_BITSTREAM params; ++ uint32_t compat_pad[2]; ++ } lock_params_padded = { 0 }; ++ NV_ENC_LOCK_BITSTREAM *lock_params = &lock_params_padded.params; + NVENCSTATUS nv_status; + int res = 0; + + enum AVPictureType pict_type; + +- lock_params.version = NV_ENC_LOCK_BITSTREAM_VER; ++ lock_params->version = ctx->api_vers->lock_bitstream; + +- lock_params.doNotWait = 0; +- lock_params.outputBitstream = tmpoutsurf->output_surface; ++ lock_params->doNotWait = 0; ++ lock_params->outputBitstream = tmpoutsurf->output_surface; + +- nv_status = p_nvenc->nvEncLockBitstream(ctx->nvencoder, &lock_params); ++ nv_status = p_nvenc->nvEncLockBitstream(ctx->nvencoder, lock_params); + if (nv_status != NV_ENC_SUCCESS) { + res = nvenc_print_error(avctx, nv_status, "Failed locking bitstream buffer"); + goto error; + } + +- res = ff_get_encode_buffer(avctx, pkt, lock_params.bitstreamSizeInBytes, 0); ++ res = ff_get_encode_buffer(avctx, pkt, lock_params->bitstreamSizeInBytes, 0); + + if (res < 0) { + p_nvenc->nvEncUnlockBitstream(ctx->nvencoder, tmpoutsurf->output_surface); + goto error; + } + +- memcpy(pkt->data, lock_params.bitstreamBufferPtr, lock_params.bitstreamSizeInBytes); ++ memcpy(pkt->data, lock_params->bitstreamBufferPtr, lock_params->bitstreamSizeInBytes); + + nv_status = p_nvenc->nvEncUnlockBitstream(ctx->nvencoder, tmpoutsurf->output_surface); + if (nv_status != NV_ENC_SUCCESS) { +@@ -2776,7 +3056,7 @@ static int process_output_surface(AVCodecContext *avctx, AVPacket *pkt, NvencSur + tmpoutsurf->input_surface = NULL; + } + +- switch (lock_params.pictureType) { ++ switch (lock_params->pictureType) { + case NV_ENC_PIC_TYPE_IDR: + pkt->flags |= AV_PKT_FLAG_KEY; + case NV_ENC_PIC_TYPE_I: +@@ -2799,13 +3079,13 @@ static int process_output_surface(AVCodecContext *avctx, AVPacket *pkt, NvencSur + } + + ff_encode_add_stats_side_data(pkt, +- (lock_params.frameAvgQP - 1) * FF_QP2LAMBDA, NULL, 0, pict_type); ++ (lock_params->frameAvgQP - 1) * FF_QP2LAMBDA, NULL, 0, pict_type); + +- res = nvenc_set_timestamp(avctx, &lock_params, pkt); ++ res = nvenc_set_timestamp(avctx, lock_params, pkt); + if (res < 0) + goto error2; + +- res = nvenc_retrieve_frame_data(avctx, &lock_params, pkt); ++ res = nvenc_retrieve_frame_data(avctx, lock_params, pkt); + if (res < 0) + goto error2; + +@@ -2948,14 +3228,21 @@ static void reconfig_encoder(AVCodecContext *avctx, const AVFrame *frame) + NV_ENCODE_API_FUNCTION_LIST *p_nvenc = &ctx->nvenc_dload_funcs.nvenc_funcs; + NVENCSTATUS ret; + +- NV_ENC_RECONFIGURE_PARAMS params = { 0 }; ++ /* The pre-12.2 NV_ENC_RECONFIGURE_PARAMS layout is 8 bytes larger than ++ * the 12.2+ layout this is compiled against; keep zeroed spare space ++ * after it for drivers negotiated to API < 12.2. */ ++ struct { ++ NV_ENC_RECONFIGURE_PARAMS params; ++ uint32_t compat_pad[2]; ++ } params_padded = { 0 }; ++ NV_ENC_RECONFIGURE_PARAMS *params = ¶ms_padded.params; + int needs_reconfig = 0; + int needs_encode_config = 0; + int reconfig_bitrate = 0, reconfig_dar = 0; + int dw, dh; + +- params.version = NV_ENC_RECONFIGURE_PARAMS_VER; +- params.reInitEncodeParams = ctx->init_encode_params; ++ params->version = ctx->api_vers->reconfigure_params; ++ params->reInitEncodeParams = ctx->init_encode_params; + + compute_dar(avctx, &dw, &dh); + if (dw != ctx->init_encode_params.darWidth || dh != ctx->init_encode_params.darHeight) { +@@ -2964,47 +3251,59 @@ static void reconfig_encoder(AVCodecContext *avctx, const AVFrame *frame) + ctx->init_encode_params.darWidth, + ctx->init_encode_params.darHeight, dw, dh); + +- params.reInitEncodeParams.darHeight = dh; +- params.reInitEncodeParams.darWidth = dw; ++ params->reInitEncodeParams.darHeight = dh; ++ params->reInitEncodeParams.darWidth = dw; + + needs_reconfig = 1; + reconfig_dar = 1; + } + + if (ctx->rc != NV_ENC_PARAMS_RC_CONSTQP && ctx->support_dyn_bitrate) { +- if (avctx->bit_rate > 0 && params.reInitEncodeParams.encodeConfig->rcParams.averageBitRate != avctx->bit_rate) { ++ if (avctx->bit_rate > 0 && params->reInitEncodeParams.encodeConfig->rcParams.averageBitRate != avctx->bit_rate) { + av_log(avctx, AV_LOG_VERBOSE, + "avg bitrate change: %d -> %d\n", +- params.reInitEncodeParams.encodeConfig->rcParams.averageBitRate, ++ params->reInitEncodeParams.encodeConfig->rcParams.averageBitRate, + (uint32_t)avctx->bit_rate); + +- params.reInitEncodeParams.encodeConfig->rcParams.averageBitRate = avctx->bit_rate; ++ params->reInitEncodeParams.encodeConfig->rcParams.averageBitRate = avctx->bit_rate; + reconfig_bitrate = 1; + } + + if (avctx->rc_max_rate > 0 && ctx->encode_config.rcParams.maxBitRate != avctx->rc_max_rate) { + av_log(avctx, AV_LOG_VERBOSE, + "max bitrate change: %d -> %d\n", +- params.reInitEncodeParams.encodeConfig->rcParams.maxBitRate, ++ params->reInitEncodeParams.encodeConfig->rcParams.maxBitRate, + (uint32_t)avctx->rc_max_rate); + +- params.reInitEncodeParams.encodeConfig->rcParams.maxBitRate = avctx->rc_max_rate; ++ params->reInitEncodeParams.encodeConfig->rcParams.maxBitRate = avctx->rc_max_rate; + reconfig_bitrate = 1; + } + + if (avctx->rc_buffer_size > 0 && ctx->encode_config.rcParams.vbvBufferSize != avctx->rc_buffer_size) { + av_log(avctx, AV_LOG_VERBOSE, + "vbv buffer size change: %d -> %d\n", +- params.reInitEncodeParams.encodeConfig->rcParams.vbvBufferSize, ++ params->reInitEncodeParams.encodeConfig->rcParams.vbvBufferSize, + avctx->rc_buffer_size); + +- params.reInitEncodeParams.encodeConfig->rcParams.vbvBufferSize = avctx->rc_buffer_size; ++ params->reInitEncodeParams.encodeConfig->rcParams.vbvBufferSize = avctx->rc_buffer_size; + reconfig_bitrate = 1; + } + + if (reconfig_bitrate) { +- params.resetEncoder = 1; +- params.forceIDR = 1; ++ if (NVENC_API_AT_LEAST(ctx, 12, 2)) { ++ params->resetEncoder = 1; ++ params->forceIDR = 1; ++ } else { ++ /* SDK 12.2 inserted a reserved word at offset 4 of ++ * NV_ENC_RECONFIGURE_PARAMS, shifting the trailing ++ * resetEncoder/forceIDR bits from byte 1816 to 1808. Pre-12.2 ++ * drivers read them at 1816, which is compat_pad[0] of the ++ * padded wrapper (verified against SDK 11.1-12.1 headers: ++ * resetEncoder = bit 0, forceIDR = bit 1). The compiled ++ * bitfields must stay 0: byte 1808 lies in reserved space of ++ * the embedded pre-12.2 reInitEncodeParams. */ ++ params_padded.compat_pad[0] = 0x1 | 0x2; ++ } + + needs_encode_config = 1; + needs_reconfig = 1; +@@ -3012,10 +3311,10 @@ static void reconfig_encoder(AVCodecContext *avctx, const AVFrame *frame) + } + + if (!needs_encode_config) +- params.reInitEncodeParams.encodeConfig = NULL; ++ params->reInitEncodeParams.encodeConfig = NULL; + + if (needs_reconfig) { +- ret = p_nvenc->nvEncReconfigureEncoder(ctx->nvencoder, ¶ms); ++ ret = p_nvenc->nvEncReconfigureEncoder(ctx->nvencoder, params); + if (ret != NV_ENC_SUCCESS) { + nvenc_print_error(avctx, ret, "failed to reconfigure nvenc"); + } else { +@@ -3025,9 +3324,9 @@ static void reconfig_encoder(AVCodecContext *avctx, const AVFrame *frame) + } + + if (reconfig_bitrate) { +- ctx->encode_config.rcParams.averageBitRate = params.reInitEncodeParams.encodeConfig->rcParams.averageBitRate; +- ctx->encode_config.rcParams.maxBitRate = params.reInitEncodeParams.encodeConfig->rcParams.maxBitRate; +- ctx->encode_config.rcParams.vbvBufferSize = params.reInitEncodeParams.encodeConfig->rcParams.vbvBufferSize; ++ ctx->encode_config.rcParams.averageBitRate = params->reInitEncodeParams.encodeConfig->rcParams.averageBitRate; ++ ctx->encode_config.rcParams.maxBitRate = params->reInitEncodeParams.encodeConfig->rcParams.maxBitRate; ++ ctx->encode_config.rcParams.vbvBufferSize = params->reInitEncodeParams.encodeConfig->rcParams.vbvBufferSize; + } + + } +@@ -3126,7 +3425,7 @@ static int nvenc_send_frame(AVCodecContext *avctx, const AVFrame *frame) + NV_ENCODE_API_FUNCTION_LIST *p_nvenc = &dl_fn->nvenc_funcs; + + NV_ENC_PIC_PARAMS pic_params = { 0 }; +- pic_params.version = NV_ENC_PIC_PARAMS_VER; ++ pic_params.version = ctx->api_vers->pic_params; + + if ((!ctx->cu_context && !ctx->d3d11_device) || !ctx->nvencoder) + return AVERROR(EINVAL); +diff --git a/libavcodec/nvenc.h b/libavcodec/nvenc.h +index 069ba82bc9..4225c612aa 100644 +--- a/libavcodec/nvenc.h ++++ b/libavcodec/nvenc.h +@@ -111,6 +111,54 @@ typedef void ID3D11Device; + #define NVENC_HAVE_MVHEVC + #endif + ++/* ++ * Runtime NVENC API version negotiation. ++ * ++ * The NVENC driver rejects clients that present a newer API version than it ++ * supports, so an FFmpeg built against new SDK headers normally refuses to ++ * run on older drivers. Instead of failing, we query the driver's maximum ++ * supported API version at runtime and present struct version words (and the ++ * session apiVersion) matching what that driver expects, while disabling ++ * features that need a newer API. This keeps one binary working from driver ++ * 470.57.02 (API 11.1, the last driver branch for Kepler / first-generation ++ * NVENC GPUs) up to current drivers, including AV1 support on drivers that ++ * have it. ++ * ++ * The struct version table and the legacy bit-depth field positions below ++ * were derived from and verified against the exact layouts of nv-codec-headers ++ * n11.1.5.3, n12.0.16.1, n12.1.14.0, n12.2.72.0 and n13.0.19.0. All struct ++ * fields FFmpeg touches sit at identical offsets in all of those versions ++ * (new fields are only ever carved out of reserved space). Building against ++ * any other headers invalidates those assumptions, so refuse it. ++ */ ++#if !NVENCAPI_CHECK_VERSION(13, 0) || NVENCAPI_CHECK_VERSION(13, 1) ++#error "The nvenc runtime API negotiation patch requires nv-codec-headers 13.0.x. Re-verify the struct version table and legacy bit-depth field layout before moving to other headers." ++#endif ++ ++typedef struct NvencApiVersion { ++ uint32_t packed; // (major << 4) | minor, the NvEncodeAPIGetMaxSupportedVersion encoding ++ uint32_t api; // NVENCAPI_VERSION of that SDK: major | (minor << 24) ++ uint32_t function_list; // NV_ENCODE_API_FUNCTION_LIST_VER ++ uint32_t open_session_params; // NV_ENC_OPEN_ENCODE_SESSION_EX_PARAMS_VER ++ uint32_t caps_param; // NV_ENC_CAPS_PARAM_VER ++ uint32_t initialize_params; // NV_ENC_INITIALIZE_PARAMS_VER ++ uint32_t config; // NV_ENC_CONFIG_VER ++ uint32_t preset_config; // NV_ENC_PRESET_CONFIG_VER ++ uint32_t pic_params; // NV_ENC_PIC_PARAMS_VER ++ uint32_t lock_bitstream; // NV_ENC_LOCK_BITSTREAM_VER ++ uint32_t lock_input_buffer; // NV_ENC_LOCK_INPUT_BUFFER_VER ++ uint32_t map_input_resource; // NV_ENC_MAP_INPUT_RESOURCE_VER ++ uint32_t register_resource; // NV_ENC_REGISTER_RESOURCE_VER ++ uint32_t create_input_buffer; // NV_ENC_CREATE_INPUT_BUFFER_VER ++ uint32_t create_bitstream_buffer; // NV_ENC_CREATE_BITSTREAM_BUFFER_VER ++ uint32_t reconfigure_params; // NV_ENC_RECONFIGURE_PARAMS_VER ++ uint32_t sequence_param_payload; // NV_ENC_SEQUENCE_PARAM_PAYLOAD_VER ++} NvencApiVersion; ++ ++#define NVENC_API_PACKED(major, minor) (((uint32_t)(major) << 4) | (minor)) ++#define NVENC_API_AT_LEAST(ctx, major, minor) \ ++ ((ctx)->api_version >= NVENC_API_PACKED(major, minor)) ++ + typedef struct NvencSurface + { + NV_ENC_INPUT_PTR input_surface; +@@ -216,6 +264,11 @@ typedef struct NvencContext + NvencDynLoadFunctions nvenc_dload_funcs; + + NV_ENC_INITIALIZE_PARAMS init_encode_params; ++ /* The pre-12.2 NV_ENC_INITIALIZE_PARAMS layout is 8 bytes larger than the ++ * 12.2+ layout this is compiled against. Drivers negotiated to API < 12.2 ++ * read that larger footprint, so keep zeroed spare space directly after ++ * the struct. Must stay right after init_encode_params and stay zero. */ ++ uint32_t init_encode_params_compat_pad[2]; + NV_ENC_CONFIG encode_config; + CUcontext cu_context; + CUcontext cu_context_internal; +@@ -321,6 +374,11 @@ typedef struct NvencContext + int cbr_padding; + int multiview, multiview_supported; + int display_sei_sent; ++ ++ /* Runtime-negotiated NVENC API version: the newest version supported by ++ * both the driver and this build. Set in nvenc_load_libraries. */ ++ const NvencApiVersion *api_vers; ++ uint32_t api_version; // negotiated, (major << 4) | minor + } NvencContext; + + int ff_nvenc_encode_init(AVCodecContext *avctx); diff --git a/subprojects/packagefiles/ffmpeg/meson.build b/subprojects/packagefiles/ffmpeg/meson.build new file mode 100644 index 0000000..38df7e9 --- /dev/null +++ b/subprojects/packagefiles/ffmpeg/meson.build @@ -0,0 +1,35 @@ +project('ffmpeg', 'c', version : '8.1') + +# Neither ffmpeg nor the libraries it depends on use meson, so they are built with their +# own build systems into a shared prefix and then exposed to gpu-screen-recorder as +# regular pkg-config dependencies. + +install_prefix = meson.current_build_dir() / 'install' +c_compiler = ' '.join(meson.get_compiler('c').cmd_array()) +build_library = find_program(meson.global_source_root() / 'extra' / 'build_ffmpeg.sh') + +# srt links against openssl and ffmpeg links against all of them, so the order matters. +libraries = ['nv-codec-headers', 'x264', 'opus', 'openssl', 'srt', 'ffmpeg'] + +foreach library : libraries + source_dir = library == 'ffmpeg' ? meson.current_source_dir() : subproject(library).get_variable('source_dir') + build_result = run_command(build_library, library, source_dir, meson.current_build_dir() / library, install_prefix, c_compiler, check : true) + if build_result.stderr().strip() != '' + message(build_result.stderr().strip()) + endif +endforeach + +ffmpeg_libs = ['libavformat', 'libavfilter', 'libavcodec', 'libswresample', 'libavutil'] + +pkgconfig = find_program('pkg-config') +pkgconfig_env = environment() +pkgconfig_env.prepend('PKG_CONFIG_PATH', install_prefix / 'lib' / 'pkgconfig') + +ffmpeg_cflags = run_command(pkgconfig, '--cflags', ffmpeg_libs, env : pkgconfig_env, check : true).stdout().split() +ffmpeg_ldflags = run_command(pkgconfig, '--static', '--libs', ffmpeg_libs, env : pkgconfig_env, check : true).stdout().split() + +ffmpeg_dep = declare_dependency(compile_args : ffmpeg_cflags, link_args : ffmpeg_ldflags) + +foreach ffmpeg_lib : ffmpeg_libs + meson.override_dependency(ffmpeg_lib, ffmpeg_dep) +endforeach diff --git a/subprojects/packagefiles/nv-codec-headers/meson.build b/subprojects/packagefiles/nv-codec-headers/meson.build new file mode 100644 index 0000000..cc59db7 --- /dev/null +++ b/subprojects/packagefiles/nv-codec-headers/meson.build @@ -0,0 +1,3 @@ +project('nv-codec-headers') + +source_dir = meson.current_source_dir() diff --git a/subprojects/packagefiles/openssl/meson.build b/subprojects/packagefiles/openssl/meson.build new file mode 100644 index 0000000..d7ebb0c --- /dev/null +++ b/subprojects/packagefiles/openssl/meson.build @@ -0,0 +1,3 @@ +project('openssl') + +source_dir = meson.current_source_dir() diff --git a/subprojects/packagefiles/opus/meson.build b/subprojects/packagefiles/opus/meson.build new file mode 100644 index 0000000..858205d --- /dev/null +++ b/subprojects/packagefiles/opus/meson.build @@ -0,0 +1,3 @@ +project('opus') + +source_dir = meson.current_source_dir() diff --git a/subprojects/packagefiles/srt/meson.build b/subprojects/packagefiles/srt/meson.build new file mode 100644 index 0000000..3fa707e --- /dev/null +++ b/subprojects/packagefiles/srt/meson.build @@ -0,0 +1,3 @@ +project('srt') + +source_dir = meson.current_source_dir() diff --git a/subprojects/packagefiles/x264/meson.build b/subprojects/packagefiles/x264/meson.build new file mode 100644 index 0000000..87f3a80 --- /dev/null +++ b/subprojects/packagefiles/x264/meson.build @@ -0,0 +1,3 @@ +project('x264') + +source_dir = meson.current_source_dir() diff --git a/subprojects/srt.wrap b/subprojects/srt.wrap new file mode 100644 index 0000000..583b2e8 --- /dev/null +++ b/subprojects/srt.wrap @@ -0,0 +1,6 @@ +[wrap-file] +directory = srt-1.5.6 +source_url = https://github.com/Haivision/srt/archive/refs/tags/v1.5.6.tar.gz +source_filename = srt-1.5.6.tar.gz +source_hash = 2c4980c2c4cfd142d21b829d939dc51db9c6628af5967fff62fd7290769569c7 +patch_directory = srt diff --git a/subprojects/x264.wrap b/subprojects/x264.wrap new file mode 100644 index 0000000..f9428cb --- /dev/null +++ b/subprojects/x264.wrap @@ -0,0 +1,6 @@ +[wrap-file] +directory = x264-b35605ace3ddf7c1a5d67a2eb553f034aef41d55 +source_url = https://code.videolan.org/videolan/x264/-/archive/b35605ace3ddf7c1a5d67a2eb553f034aef41d55/x264-b35605ace3ddf7c1a5d67a2eb553f034aef41d55.tar.bz2 +source_filename = x264-b35605ace3ddf7c1a5d67a2eb553f034aef41d55.tar.bz2 +source_hash = 6eeb82934e69fd51e043bd8c5b0d152839638d1ce7aa4eea65a3fedcf83ff224 +patch_directory = x264 -- cgit v1.2.3 From b2f903ba32c8b14fb0e498dc01a53679adc1476e Mon Sep 17 00:00:00 2001 From: dec05eba Date: Wed, 5 Aug 2026 03:23:07 +0200 Subject: Use mbedtls instead of openssl --- README.md | 6 +- extra/build_ffmpeg.sh | 76 ++++++--------------- subprojects/ffmpeg.wrap | 2 +- subprojects/mbedtls.wrap | 6 ++ subprojects/openssl.wrap | 6 -- .../ffmpeg-mbedtls-default-ca-certs.patch | 79 ++++++++++++++++++++++ .../ffmpeg-mbedtls-skip-non-dtls-packets.patch | 48 +++++++++++++ subprojects/packagefiles/ffmpeg/meson.build | 4 +- subprojects/packagefiles/mbedtls/meson.build | 3 + subprojects/packagefiles/openssl/meson.build | 3 - 10 files changed, 164 insertions(+), 69 deletions(-) create mode 100644 subprojects/mbedtls.wrap delete mode 100644 subprojects/openssl.wrap create mode 100644 subprojects/packagefiles/ffmpeg-mbedtls-default-ca-certs.patch create mode 100644 subprojects/packagefiles/ffmpeg-mbedtls-skip-non-dtls-packets.patch create mode 100644 subprojects/packagefiles/mbedtls/meson.build delete mode 100644 subprojects/packagefiles/openssl/meson.build diff --git a/README.md b/README.md index 9403e23..055bddd 100644 --- a/README.md +++ b/README.md @@ -76,8 +76,10 @@ When building GPU Screen Recorder with portal support (`-Dportal=true` meson opt * libpipewire (and libspa which is usually part of libpipewire) ## Building with a statically linked ffmpeg -The `-Dffmpeg_static=true` meson option (disabled by default) downloads ffmpeg and the libraries that ffmpeg depends on (x264, opus, openssl, srt and nv-codec-headers), builds them from source with lto and only the components that GPU Screen Recorder uses and then links them statically into GPU Screen Recorder. -This removes the runtime dependency on the system ffmpeg and on all of those libraries. `make`, `nasm`, `cmake` and `perl` are needed to build them and the first `meson setup` takes a few minutes longer because of it. +The `-Dffmpeg_static=true` meson option (disabled by default) downloads ffmpeg and the libraries that ffmpeg depends on (x264, opus, mbedtls, srt and nv-codec-headers), builds them from source with lto and only the components that GPU Screen Recorder uses and then links them statically into GPU Screen Recorder. +This removes the runtime dependency on the system ffmpeg and on all of those libraries. `make`, `nasm`, `cmake` and `python3` are needed to build them and the first `meson setup` takes a few minutes longer because of it. + +mbedtls is used as the tls backend instead of openssl because it's much smaller. mbedtls has no built-in default certificate location, so ffmpeg is patched to look for the certificate store of the system (and to honor the `SSL_CERT_FILE` and `SSL_CERT_DIR` environment variables) the same way that openssl does. The only libraries that ffmpeg is still dynamically linked to are libva, libdrm (which GPU Screen Recorder also uses directly) and the c/c++ runtime. diff --git a/extra/build_ffmpeg.sh b/extra/build_ffmpeg.sh index 06c3344..052a822 100755 --- a/extra/build_ffmpeg.sh +++ b/extra/build_ffmpeg.sh @@ -69,58 +69,24 @@ case $library in make install } ;; - openssl) - require_program perl - # The certificate directory of the system openssl is used so that certificate - # verification keeps working the same way as it does with the system openssl. - openssl_dir=$(openssl version -d 2>/dev/null | sed -n 's/^OPENSSLDIR: "\(.*\)"$/\1/p') - if [ -z "$openssl_dir" ]; then - openssl_dir=/etc/ssl - fi - set -- --prefix="$prefix" \ - --libdir=lib \ - --openssldir="$openssl_dir" \ - no-shared \ - no-apps \ - no-docs \ - no-tests \ - no-legacy \ - no-engine \ - no-comp \ - no-quic \ - no-ssl3 \ - no-weak-ssl-ciphers \ - no-cms \ - no-ct \ - no-ts \ - no-ocsp \ - no-srp \ - no-psk \ - no-dsa \ - no-ec2m \ - no-gost \ - no-idea \ - no-md2 \ - no-md4 \ - no-mdc2 \ - no-rc2 \ - no-rc4 \ - no-rc5 \ - no-bf \ - no-cast \ - no-seed \ - no-camellia \ - no-whirlpool \ - no-rmd160 \ - no-sm2 \ - no-sm3 \ - no-sm4 \ - no-siphash \ - -O3 -flto -fPIC + mbedtls) + require_program cmake + require_program python3 + set -- -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_INSTALL_PREFIX="$prefix" \ + -DCMAKE_INSTALL_LIBDIR=lib \ + -DCMAKE_INTERPROCEDURAL_OPTIMIZATION=ON \ + -DCMAKE_POSITION_INDEPENDENT_CODE=ON \ + -DENABLE_TESTING=OFF \ + -DENABLE_PROGRAMS=OFF \ + -DUSE_SHARED_MBEDTLS_LIBRARY=OFF \ + -DUSE_STATIC_MBEDTLS_LIBRARY=ON build() { - "$source_dir/Configure" "$@" - make -j"$jobs" - make install_sw + # The whip muxer needs dtls-srtp, which mbedtls doesn't enable by default. + python3 "$source_dir/scripts/config.py" -f "$source_dir/include/mbedtls/mbedtls_config.h" set MBEDTLS_SSL_DTLS_SRTP + cmake -S "$source_dir" -B "$build_dir" "$@" + cmake --build "$build_dir" -j"$jobs" + cmake --install "$build_dir" } ;; srt) @@ -136,9 +102,9 @@ case $library in -DENABLE_EXAMPLES=OFF \ -DENABLE_TESTING=OFF \ -DENABLE_UNITTESTS=OFF \ - -DUSE_ENCLIB=openssl \ - -DOPENSSL_ROOT_DIR="$prefix" \ - -DOPENSSL_USE_STATIC_LIBS=ON + -DUSE_ENCLIB=mbedtls \ + -DMBEDTLS_PREFIX="$prefix" \ + -DSSL_REQUIRED_MODULES=mbedtls build() { cmake -S "$source_dir" -B "$build_dir" "$@" cmake --build "$build_dir" -j"$jobs" @@ -192,7 +158,7 @@ case $library in --enable-libx264 \ --enable-libopus \ --enable-libsrt \ - --enable-openssl \ + --enable-mbedtls \ --enable-ffnvcodec \ --enable-nvenc \ --enable-cuda \ diff --git a/subprojects/ffmpeg.wrap b/subprojects/ffmpeg.wrap index 56e8e65..c98e19a 100644 --- a/subprojects/ffmpeg.wrap +++ b/subprojects/ffmpeg.wrap @@ -4,4 +4,4 @@ source_url = https://ffmpeg.org/releases/ffmpeg-8.1.tar.xz source_filename = ffmpeg-8.1.tar.xz source_hash = b072aed6871998cce9b36e7774033105ca29e33632be5b6347f3206898e0756a patch_directory = ffmpeg -diff_files = ffmpeg-nvenc-runtime-api-version.patch +diff_files = ffmpeg-nvenc-runtime-api-version.patch, ffmpeg-mbedtls-skip-non-dtls-packets.patch, ffmpeg-mbedtls-default-ca-certs.patch diff --git a/subprojects/mbedtls.wrap b/subprojects/mbedtls.wrap new file mode 100644 index 0000000..dfd3c32 --- /dev/null +++ b/subprojects/mbedtls.wrap @@ -0,0 +1,6 @@ +[wrap-file] +directory = mbedtls-3.6.7 +source_url = https://github.com/Mbed-TLS/mbedtls/releases/download/mbedtls-3.6.7/mbedtls-3.6.7.tar.bz2 +source_filename = mbedtls-3.6.7.tar.bz2 +source_hash = a7e8bcbec0e6f761b4af24f25677626b35f762f68eef79c08677a363212d11f6 +patch_directory = mbedtls diff --git a/subprojects/openssl.wrap b/subprojects/openssl.wrap deleted file mode 100644 index fe386af..0000000 --- a/subprojects/openssl.wrap +++ /dev/null @@ -1,6 +0,0 @@ -[wrap-file] -directory = openssl-3.6.3 -source_url = https://github.com/openssl/openssl/releases/download/openssl-3.6.3/openssl-3.6.3.tar.gz -source_filename = openssl-3.6.3.tar.gz -source_hash = 243a86649cf6f23eeb6a2ff2456e09e5d77dd9018a54d3d96b0c6bdd6ba6c7f1 -patch_directory = openssl diff --git a/subprojects/packagefiles/ffmpeg-mbedtls-default-ca-certs.patch b/subprojects/packagefiles/ffmpeg-mbedtls-default-ca-certs.patch new file mode 100644 index 0000000..dc2c45f --- /dev/null +++ b/subprojects/packagefiles/ffmpeg-mbedtls-default-ca-certs.patch @@ -0,0 +1,79 @@ +--- a/libavformat/tls_mbedtls.c ++++ b/libavformat/tls_mbedtls.c +@@ -42,6 +42,66 @@ + #include "libavutil/avstring.h" + #include "libavutil/random_seed.h" + #include "libavutil/intreadwrite.h" ++#include "libavutil/getenv_utf8.h" ++ ++/* ++ * mbedtls has no built-in default certificate location, unlike openssl which ++ * falls back to the location it was compiled with. Without this the peer ++ * certificate can only be verified when the caller passes a ca_file, so look ++ * for the certificate store of the system instead, honoring the same ++ * environment variables as openssl does. ++ */ ++static const char * const default_ca_files[] = { ++ "/etc/ssl/certs/ca-certificates.crt", // debian, ubuntu, arch, alpine, gentoo ++ "/etc/pki/tls/certs/ca-bundle.crt", // fedora, rhel ++ "/etc/ssl/ca-bundle.pem", // opensuse ++ "/etc/ssl/cert.pem", // openbsd, freebsd, macos ++ "/usr/local/etc/ssl/cert.pem", // freebsd ports ++}; ++ ++static const char * const default_ca_dirs[] = { ++ "/etc/ssl/certs", ++ "/etc/pki/tls/certs", ++}; ++ ++/* A positive return value from mbedtls means that only some of the certificates failed to parse, which is not fatal */ ++static int mbedtls_load_default_ca_certs(URLContext *h, mbedtls_x509_crt *ca_cert) ++{ ++ char *env_ca_file = getenv_utf8("SSL_CERT_FILE"); ++ char *env_ca_dir = getenv_utf8("SSL_CERT_DIR"); ++ int loaded = 0; ++ ++ if (env_ca_file && mbedtls_x509_crt_parse_file(ca_cert, env_ca_file) >= 0) ++ loaded = 1; ++ ++ if (!loaded && env_ca_dir && mbedtls_x509_crt_parse_path(ca_cert, env_ca_dir) >= 0) ++ loaded = 1; ++ ++ freeenv_utf8(env_ca_file); ++ freeenv_utf8(env_ca_dir); ++ ++ for (size_t i = 0; !loaded && i < FF_ARRAY_ELEMS(default_ca_files); i++) { ++ if (mbedtls_x509_crt_parse_file(ca_cert, default_ca_files[i]) >= 0) { ++ av_log(h, AV_LOG_VERBOSE, "loaded CA certificates from %s\n", default_ca_files[i]); ++ loaded = 1; ++ } ++ } ++ ++ for (size_t i = 0; !loaded && i < FF_ARRAY_ELEMS(default_ca_dirs); i++) { ++ if (mbedtls_x509_crt_parse_path(ca_cert, default_ca_dirs[i]) >= 0) { ++ av_log(h, AV_LOG_VERBOSE, "loaded CA certificates from %s\n", default_ca_dirs[i]); ++ loaded = 1; ++ } ++ } ++ ++ if (!loaded) { ++ av_log(h, AV_LOG_WARNING, "unable to find the CA certificates of the system, " ++ "certificate verification is going to fail\n"); ++ return AVERROR(ENOENT); ++ } ++ ++ return 0; ++} + + static int mbedtls_x509_fingerprint(char *cert_buf, size_t cert_sz, char **fingerprint) + { +@@ -557,6 +617,9 @@ + av_log(h, AV_LOG_ERROR, "mbedtls_x509_crt_parse_file for CA cert returned %d\n", ret); + goto fail; + } ++ } else if (shr->verify) { ++ // Only a warning is logged when this fails, matching what the openssl backend does ++ mbedtls_load_default_ca_certs(h, &tls_ctx->ca_cert); + } + + // load own certificate diff --git a/subprojects/packagefiles/ffmpeg-mbedtls-skip-non-dtls-packets.patch b/subprojects/packagefiles/ffmpeg-mbedtls-skip-non-dtls-packets.patch new file mode 100644 index 0000000..4f18644 --- /dev/null +++ b/subprojects/packagefiles/ffmpeg-mbedtls-skip-non-dtls-packets.patch @@ -0,0 +1,48 @@ +--- a/libavformat/tls_mbedtls.c ++++ b/libavformat/tls_mbedtls.c +@@ -41,6 +41,7 @@ + #include "libavutil/parseutils.h" + #include "libavutil/avstring.h" + #include "libavutil/random_seed.h" ++#include "libavutil/intreadwrite.h" + + static int mbedtls_x509_fingerprint(char *cert_buf, size_t cert_sz, char **fingerprint) + { +@@ -376,6 +377,27 @@ + return handle_transport_error(h, "ffurl_write", MBEDTLS_ERR_SSL_WANT_WRITE, ret); + } + ++/* ++ * Some webrtc servers, such as the ones based on pion, send stun packets during the ++ * dtls handshake. openssl and gnutls filter those out internally but mbedtls passes ++ * every received udp packet to its dtls state machine, which makes the handshake fail. ++ * This is the same check as the one in whip.c, which ffmpeg n8.1 doesn't share yet. ++ */ ++#define DTLS_CONTENT_TYPE_CHANGE_CIPHER_SPEC 20 ++#define DTLS_RECORD_LAYER_HEADER_LEN 13 ++#define DTLS_VERSION_10 0xfeff ++#define DTLS_VERSION_12 0xfefd ++ ++static int is_dtls_packet(const unsigned char *buf, int size) ++{ ++ if (size > DTLS_RECORD_LAYER_HEADER_LEN) { ++ uint16_t version = AV_RB16(&buf[1]); ++ return buf[0] >= DTLS_CONTENT_TYPE_CHANGE_CIPHER_SPEC && ++ (version == DTLS_VERSION_10 || version == DTLS_VERSION_12); ++ } ++ return 0; ++} ++ + static int mbedtls_recv(void *ctx, unsigned char *buf, size_t len) + { + TLSContext *tls_ctx = (TLSContext*) ctx; +@@ -394,6 +416,9 @@ + } + av_log(tls_ctx, AV_LOG_TRACE, "Set UDP remote addr on UDP socket, now 'connected'\n"); + } ++ /* Skip non-DTLS packets such as STUN to avoid failures. */ ++ if (shr->is_dtls && !is_dtls_packet(buf, ret)) ++ return MBEDTLS_ERR_SSL_WANT_READ; + return ret; + } + if (h->max_packet_size && len > h->max_packet_size) diff --git a/subprojects/packagefiles/ffmpeg/meson.build b/subprojects/packagefiles/ffmpeg/meson.build index 38df7e9..96345f2 100644 --- a/subprojects/packagefiles/ffmpeg/meson.build +++ b/subprojects/packagefiles/ffmpeg/meson.build @@ -8,8 +8,8 @@ install_prefix = meson.current_build_dir() / 'install' c_compiler = ' '.join(meson.get_compiler('c').cmd_array()) build_library = find_program(meson.global_source_root() / 'extra' / 'build_ffmpeg.sh') -# srt links against openssl and ffmpeg links against all of them, so the order matters. -libraries = ['nv-codec-headers', 'x264', 'opus', 'openssl', 'srt', 'ffmpeg'] +# srt links against mbedtls and ffmpeg links against all of them, so the order matters. +libraries = ['nv-codec-headers', 'x264', 'opus', 'mbedtls', 'srt', 'ffmpeg'] foreach library : libraries source_dir = library == 'ffmpeg' ? meson.current_source_dir() : subproject(library).get_variable('source_dir') diff --git a/subprojects/packagefiles/mbedtls/meson.build b/subprojects/packagefiles/mbedtls/meson.build new file mode 100644 index 0000000..802357e --- /dev/null +++ b/subprojects/packagefiles/mbedtls/meson.build @@ -0,0 +1,3 @@ +project('mbedtls') + +source_dir = meson.current_source_dir() diff --git a/subprojects/packagefiles/openssl/meson.build b/subprojects/packagefiles/openssl/meson.build deleted file mode 100644 index d7ebb0c..0000000 --- a/subprojects/packagefiles/openssl/meson.build +++ /dev/null @@ -1,3 +0,0 @@ -project('openssl') - -source_dir = meson.current_source_dir() -- cgit v1.2.3 From a522748ed2fc04e4ccad26c1da1056472cb28bc2 Mon Sep 17 00:00:00 2001 From: dec05eba Date: Wed, 5 Aug 2026 13:03:01 +0200 Subject: Remove .crt certificate check --- subprojects/packagefiles/ffmpeg-mbedtls-default-ca-certs.patch | 2 -- 1 file changed, 2 deletions(-) diff --git a/subprojects/packagefiles/ffmpeg-mbedtls-default-ca-certs.patch b/subprojects/packagefiles/ffmpeg-mbedtls-default-ca-certs.patch index dc2c45f..a22d4ae 100644 --- a/subprojects/packagefiles/ffmpeg-mbedtls-default-ca-certs.patch +++ b/subprojects/packagefiles/ffmpeg-mbedtls-default-ca-certs.patch @@ -14,8 +14,6 @@ + * environment variables as openssl does. + */ +static const char * const default_ca_files[] = { -+ "/etc/ssl/certs/ca-certificates.crt", // debian, ubuntu, arch, alpine, gentoo -+ "/etc/pki/tls/certs/ca-bundle.crt", // fedora, rhel + "/etc/ssl/ca-bundle.pem", // opensuse + "/etc/ssl/cert.pem", // openbsd, freebsd, macos + "/usr/local/etc/ssl/cert.pem", // freebsd ports -- cgit v1.2.3 From fbc31a9561d16418226c0efa65e2a73923516dbd Mon Sep 17 00:00:00 2001 From: dec05eba Date: Wed, 5 Aug 2026 14:56:08 +0200 Subject: ipc: response with video path in save recording/replay --- README.md | 5 + gpu-screen-recorder.1 | 37 ++- gsr-cli.1 | 31 ++- include/cli/ipc.h | 46 ++++ include/recorder/recorder.h | 7 + src/cli/ipc.c | 545 ++++++++++++++++++++++++++++++++++++++------ src/cli/main.c | 72 +++++- src/recorder/recorder.c | 166 +++++++++----- src/recorder/replay_save.c | 2 +- tools/gsr-cli/main.c | 103 ++++++--- 10 files changed, 843 insertions(+), 171 deletions(-) diff --git a/README.md b/README.md index 055bddd..9715748 100644 --- a/README.md +++ b/README.md @@ -161,13 +161,18 @@ and then use the `gsr-cli` program to send commands to it:\ gsr-cli -ipc "$XDG_RUNTIME_DIR/gsr.sock" save-replay 30 gsr-cli -ipc "$XDG_RUNTIME_DIR/gsr.sock" save-replay gsr-cli -ipc "$XDG_RUNTIME_DIR/gsr.sock" toggle-replay-recording +gsr-cli -ipc "$XDG_RUNTIME_DIR/gsr.sock" start-replay-recording +gsr-cli -ipc "$XDG_RUNTIME_DIR/gsr.sock" stop-replay-recording gsr-cli -ipc "$XDG_RUNTIME_DIR/gsr.sock" toggle-pause +gsr-cli -ipc "$XDG_RUNTIME_DIR/gsr.sock" set-paused true gsr-cli -ipc "$XDG_RUNTIME_DIR/gsr.sock" stop gsr-cli -ipc "$XDG_RUNTIME_DIR/gsr.sock" status ``` This gives the same control as the signals, with these differences: * `save-replay` takes the number of seconds to save, instead of the fixed times that the signals provide. The whole replay buffer is saved when no number of seconds is given. * `gsr-cli` exits with 0 only when the command succeeded and prints the reason to stderr when it didn't, so commands don't have to be sent blindly. +* `stop`, `save-replay` and `stop-replay-recording` are replied to when the file they save has been saved (and after the `-sc` script has been started), and `gsr-cli` prints the path of the saved file. +* `set-paused`, `start-replay-recording` and `stop-replay-recording` set an absolute state instead of toggling, so the result doesn't depend on the current state. * `status` prints `running` or `not running` and exits with 0 when GPU Screen Recorder is running, which a script can use to only start replay when it isn't already running: `gsr-cli -ipc "$XDG_RUNTIME_DIR/gsr.sock" status >/dev/null || start-replay.sh`. * The commands are sent to one specific GPU Screen Recorder instance instead of every instance that `pkill` matches. diff --git a/gpu-screen-recorder.1 b/gpu-screen-recorder.1 index af723e8..67857a8 100644 --- a/gpu-screen-recorder.1 +++ b/gpu-screen-recorder.1 @@ -506,27 +506,54 @@ or .B data Optional. For an .B error -result this is a string that describes what went wrong. +result this is a string that describes what went wrong. For an +.B ok +result to +.BR stop ", " save-replay " or " stop-replay-recording +this is a string with the path of the saved file. +.PP +The +.BR stop ", " save-replay " and " stop-replay-recording +requests save a file, and they are replied to when the file has been saved and the script given with the +.B \-sc +option has been started. The other requests are replied to as soon as they are accepted. .PP These requests are available: .TP .B stop -Stop and save recording (stop without save in replay mode). +Stop and save recording (stop without save in replay mode). The reply contains the path of the +saved file, except in replay mode where nothing is saved. .TP .B save-replay Save replay (replay mode only). .B data is the number of seconds to save, which has to be larger than 0. The whole replay buffer is saved when .B data -is omitted or null. +is omitted or null. The reply contains the path of the saved file. .TP .B toggle-pause Pause/unpause recording (not for streaming/replay). .TP +.B set-paused +Pause/unpause recording (not for streaming/replay). +.B data +has to be true to pause or false to unpause. Unlike +.B toggle-pause +this doesn't fail when the recording is already paused/unpaused. +.TP .B toggle-replay-recording Start/stop regular recording during replay/streaming, which requires the .B \-ro option. +.TP +.B start-replay-recording +Start regular recording during replay/streaming, which requires the +.B \-ro +option. Does nothing when a recording is already running. +.TP +.B stop-replay-recording +Stop the regular recording that runs during replay/streaming. Fails when no recording is running. +The reply contains the path of the saved file. .PP Example: .nf @@ -535,10 +562,10 @@ gpu-screen-recorder -w screen -f 60 -c mp4 -r 60 -o ~/Videos -ipc "$XDG_RUNTIME_ echo '{"id":1,"name":"save-replay","data":30}' | socat - "UNIX-CONNECT:$XDG_RUNTIME_DIR/gsr.sock" .RE .fi -which replies with: +which replies with the following when the replay has been saved: .nf .RS -{"id":1,"result":"ok"} +{"id":1,"result":"ok","data":"/home/user/Videos/Replay_2026-08-05_14-04-22.mp4"} .RE .fi .SH EXAMPLES diff --git a/gsr-cli.1 b/gsr-cli.1 index 0c6084e..0a05df9 100644 --- a/gsr-cli.1 +++ b/gsr-cli.1 @@ -41,20 +41,39 @@ and exits with 0 when it's running. A socket file that was left behind by a GPU instance that was killed counts as not running. .TP .B stop -Stop and save recording (stop without save in replay mode). +Stop and save recording (stop without save in replay mode). Waits until the recording has been +saved and prints the path of the saved file. Nothing is printed in replay mode since nothing is +saved. .TP .B toggle-pause Pause/unpause recording (not for streaming/replay). .TP +.BR set-paused " true|false" +Pause/unpause recording (not for streaming/replay). Unlike +.B toggle-pause +this doesn't fail when the recording is already paused/unpaused, so the result doesn't depend on +the current state. +.TP .B toggle-replay-recording Start/stop regular recording during replay/streaming, which requires GPU Screen Recorder to run with the .B \-ro option. .TP +.B start-replay-recording +Start regular recording during replay/streaming, which requires GPU Screen Recorder to run +with the +.B \-ro +option. Does nothing when a recording is already running. +.TP +.B stop-replay-recording +Stop the regular recording that runs during replay/streaming. Waits until the recording has been +saved and prints the path of the saved file. Fails when no recording is running. +.TP .BR save-replay " [" \fIseconds\fR ] Save replay (replay mode only). The number of seconds has to be larger than 0. The whole replay -buffer is saved when no number of seconds is given. +buffer is saved when no number of seconds is given. Waits until the replay has been saved and +prints the path of the saved file. .SH EXAMPLES Start GPU Screen Recorder in replay mode with an ipc socket: .RS @@ -80,8 +99,12 @@ gsr-cli -ipc "$XDG_RUNTIME_DIR/gsr.sock" status >/dev/null || start-replay.sh .IP \(bu 3 The socket can only be used by the user that started GPU Screen Recorder. .IP \(bu 3 -A command is replied to as soon as GPU Screen Recorder accepts it. Saving a video finishes -after the reply, and the path to the saved video is printed by GPU Screen Recorder itself. +The +.BR stop ", " save-replay " and " stop-replay-recording +commands are replied to when the file has been saved and the script given with the +.B \-sc +option has been started, and the reply contains the path of the saved file. The other commands are +replied to as soon as GPU Screen Recorder accepts them. .IP \(bu 3 GPU Screen Recorder creates the socket before it has finished starting up, so a command that is sent while it's starting up is replied to when the recording starts. diff --git a/include/cli/ipc.h b/include/cli/ipc.h index 0862c71..6008db9 100644 --- a/include/cli/ipc.h +++ b/include/cli/ipc.h @@ -3,43 +3,82 @@ #include #include +#include #include #include #define GSR_IPC_MAX_CLIENTS 8 #define GSR_IPC_MAX_REQUEST_SIZE 4096 #define GSR_IPC_MAX_ERROR_MESSAGE_SIZE 256 +#define GSR_IPC_MAX_ESCAPED_ERROR_MESSAGE_SIZE (GSR_IPC_MAX_ERROR_MESSAGE_SIZE*6) +#define GSR_IPC_MAX_ESCAPED_DATA_SIZE PATH_MAX +#define GSR_IPC_MAX_REPLY_SIZE (GSR_IPC_MAX_ESCAPED_ERROR_MESSAGE_SIZE + GSR_IPC_MAX_ESCAPED_DATA_SIZE + 128) +#define GSR_IPC_CLIENT_SEND_BUFFER_SIZE (GSR_IPC_MAX_REPLY_SIZE*2) /* These are called from the ipc thread while the recording is running. Return false and write a message to |error_message| to reply to the request with an error. + The reply to the requests that match a |gsr_ipc_deferred_request_type| is not sent when the handler + succeeds. It's sent when the matching gsr_ipc_complete_request is called. */ typedef struct { bool (*stop)(char *error_message, size_t error_message_size, void *userdata); bool (*toggle_pause)(char *error_message, size_t error_message_size, void *userdata); + bool (*set_paused)(bool paused, char *error_message, size_t error_message_size, void *userdata); bool (*toggle_replay_recording)(char *error_message, size_t error_message_size, void *userdata); + bool (*start_replay_recording)(char *error_message, size_t error_message_size, void *userdata); + bool (*stop_replay_recording)(char *error_message, size_t error_message_size, void *userdata); /* |seconds| is GSR_SAVE_REPLAY_SECONDS_FULL when the whole replay buffer should be saved */ bool (*save_replay)(int seconds, char *error_message, size_t error_message_size, void *userdata); void *userdata; } gsr_ipc_handlers; +/* Requests that are replied to when the file they save has been saved (see gsr_ipc_complete_request) */ +typedef enum { + GSR_IPC_DEFERRED_REQUEST_STOP, + GSR_IPC_DEFERRED_REQUEST_SAVE_REPLAY, + GSR_IPC_DEFERRED_REQUEST_STOP_REPLAY_RECORDING, + GSR_IPC_DEFERRED_REQUEST_TYPE_COUNT +} gsr_ipc_deferred_request_type; + +typedef enum { + GSR_IPC_DEFERRED_REQUEST_STATE_EMPTY, + GSR_IPC_DEFERRED_REQUEST_STATE_PENDING, + GSR_IPC_DEFERRED_REQUEST_STATE_COMPLETED +} gsr_ipc_deferred_request_state; + +typedef struct { + gsr_ipc_deferred_request_state state; + int client_fd; + int64_t request_id; + bool success; + bool has_filepath; + char filepath[PATH_MAX]; +} gsr_ipc_deferred_request; + typedef struct { int fd; char request[GSR_IPC_MAX_REQUEST_SIZE]; size_t request_size; bool request_too_large; + char send_buffer[GSR_IPC_CLIENT_SEND_BUFFER_SIZE]; + size_t send_buffer_size; } gsr_ipc_client; /* Receives newline terminated json requests on a unix domain socket and replies to every request */ typedef struct { bool initialized; int socket_fd; + int poll_fd; bool socket_bound; char socket_filepath[PATH_MAX]; int wakeup_pipe[2]; gsr_ipc_client clients[GSR_IPC_MAX_CLIENTS]; int num_clients; gsr_ipc_handlers handlers; + gsr_ipc_deferred_request deferred_requests[GSR_IPC_DEFERRED_REQUEST_TYPE_COUNT]; + pthread_mutex_t deferred_requests_mutex; + bool deferred_requests_mutex_created; pthread_t thread; bool thread_running; } gsr_ipc; @@ -53,4 +92,11 @@ int gsr_ipc_start(gsr_ipc *self, const gsr_ipc_handlers *handlers); /* Stops handling requests. Does nothing if the ipc hasn't been started */ void gsr_ipc_stop(gsr_ipc *self); +/* + Sends the reply to the pending request of |type|, or does nothing when there is no pending request of that type. + |filepath| is the path of the saved file and it can be NULL when nothing was saved. + This is safe to call from any thread. Does nothing if |self| hasn't been initialized. +*/ +void gsr_ipc_complete_request(gsr_ipc *self, gsr_ipc_deferred_request_type type, bool success, const char *filepath); + #endif /* GSR_CLI_IPC_H */ diff --git a/include/recorder/recorder.h b/include/recorder/recorder.h index bbd37e1..887b4fe 100644 --- a/include/recorder/recorder.h +++ b/include/recorder/recorder.h @@ -47,7 +47,14 @@ int gsr_recorder_run(gsr_recorder *self); /* These are safe to call from a signal handler or from another thread */ void gsr_recorder_stop(gsr_recorder *self); void gsr_recorder_toggle_pause(gsr_recorder *self); +/* Does nothing when the recording is already paused/unpaused */ +void gsr_recorder_set_paused(gsr_recorder *self, bool paused); void gsr_recorder_toggle_replay_recording(gsr_recorder *self); +/* Does nothing when a recording is already running */ +void gsr_recorder_start_replay_recording(gsr_recorder *self); +/* Does nothing when no recording is running */ +void gsr_recorder_stop_replay_recording(gsr_recorder *self); +bool gsr_recorder_is_replay_recording(const gsr_recorder *self); /* |seconds| can be GSR_SAVE_REPLAY_SECONDS_FULL to save the whole replay buffer */ void gsr_recorder_save_replay(gsr_recorder *self, int seconds); diff --git a/src/cli/ipc.c b/src/cli/ipc.c index b9d0c51..ff4b852 100644 --- a/src/cli/ipc.c +++ b/src/cli/ipc.c @@ -17,12 +17,21 @@ #include #include +#ifdef __linux__ +#include +#else +#include +#include +#endif + #define GSR_IPC_MAX_REQUEST_NAME_SIZE 64 -#define GSR_IPC_MAX_ESCAPED_ERROR_MESSAGE_SIZE (GSR_IPC_MAX_ERROR_MESSAGE_SIZE*6) -#define GSR_IPC_MAX_REPLY_SIZE (GSR_IPC_MAX_ESCAPED_ERROR_MESSAGE_SIZE + 128) -#define GSR_IPC_SEND_TIMEOUT_MILLISECONDS 1000 +#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]; @@ -30,6 +39,12 @@ typedef struct { bool has_data; } 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') @@ -38,7 +53,103 @@ static bool string_is_only_whitespace(const char *str, size_t size) { return true; } -static bool ipc_send_all(int fd, const char *data, size_t size) { +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); @@ -56,7 +167,7 @@ static bool ipc_send_all(int fd, const char *data, size_t size) { poll_fd.events = POLLOUT; poll_fd.revents = 0; - const int poll_result = poll(&poll_fd, 1, GSR_IPC_SEND_TIMEOUT_MILLISECONDS); + const int poll_result = poll(&poll_fd, 1, GSR_IPC_SHUTDOWN_SEND_TIMEOUT_MILLISECONDS); if(poll_result == -1 && errno == EINTR) continue; @@ -71,11 +182,76 @@ static bool ipc_send_all(int fd, const char *data, size_t size) { return true; } -static bool ipc_client_send_reply(gsr_ipc_client *client, int64_t id, bool success, const char *error_message) { +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) { + 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]; @@ -88,7 +264,7 @@ static bool ipc_client_send_reply(gsr_ipc_client *client, int64_t id, bool succe return false; } - return ipc_send_all(client->fd, reply, reply_size); + 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) { @@ -165,6 +341,76 @@ static bool ipc_request_get_save_replay_seconds(const gsr_ipc_request *request, 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); @@ -172,9 +418,23 @@ static bool ipc_handle_request(gsr_ipc *self, const gsr_ipc_request *request, ch 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; if(!ipc_request_get_save_replay_seconds(request, &seconds, error_message, error_message_size)) @@ -189,22 +449,35 @@ static bool ipc_handle_request(gsr_ipc *self, const gsr_ipc_request *request, ch static bool ipc_client_on_request(gsr_ipc *self, gsr_ipc_client *client) { if(client->request_too_large) - return ipc_client_send_reply(client, 0, false, "the request is 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(client, 0, false, "the request is empty"); + 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(client, request.id, false, error_message); + return ipc_client_send_reply(self, client, request.id, false, error_message, NULL); - if(!ipc_handle_request(self, &request, error_message, sizeof(error_message))) - return ipc_client_send_reply(client, request.id, false, error_message); + /* 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); - return ipc_client_send_reply(client, request.id, true, 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) { @@ -243,16 +516,6 @@ static bool ipc_client_receive(gsr_ipc *self, gsr_ipc_client *client) { } } -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; -} - 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"); @@ -260,7 +523,7 @@ static void ipc_add_client(gsr_ipc *self, int client_fd) { return; } - if(!fd_set_cloexec(client_fd) || !fd_set_nonblocking(client_fd)) { + 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; @@ -270,70 +533,175 @@ static void ipc_add_client(gsr_ipc *self, int client_fd) { 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_client(gsr_ipc *self) { - const int client_fd = accept(self->socket_fd, NULL, NULL); - if(client_fd == -1) - return; +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); + ipc_add_client(self, client_fd); + } } static void ipc_remove_client(gsr_ipc *self, int index) { - close(self->clients[index].fd); + 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 void* ipc_thread(void *userdata) { - gsr_ipc *self = userdata; - struct pollfd poll_fds[2 + GSR_IPC_MAX_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(;;) { - poll_fds[0].fd = self->wakeup_pipe[0]; - poll_fds[0].events = POLLIN; - poll_fds[0].revents = 0; - poll_fds[1].fd = self->socket_fd; - poll_fds[1].events = POLLIN; - poll_fds[1].revents = 0; - - const int num_polled_clients = self->num_clients; - for(int i = 0; i < num_polled_clients; ++i) { - poll_fds[2 + i].fd = self->clients[i].fd; - poll_fds[2 + i].events = POLLIN; - poll_fds[2 + i].revents = 0; + 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; +} - if(poll(poll_fds, 2 + num_polled_clients, -1) == -1) { - if(errno == EINTR) - continue; +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); - gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_ipc: failed to poll the ipc sockets, error: %s", strerror(errno)); + 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; } - if(poll_fds[0].revents != 0) - 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(poll_fds[1].revents & POLLIN) - ipc_accept_client(self); + 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; - for(int i = num_polled_clients - 1; i >= 0; --i) { + gsr_ipc_client *client = &self->clients[client_index]; bool keep_client = true; - if(poll_fds[2 + i].revents & POLLIN) - keep_client = ipc_client_receive(self, &self->clients[i]); - else if(poll_fds[2 + i].revents & (POLLHUP | POLLERR | POLLNVAL)) - keep_client = false; + 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, i); + ipc_remove_client(self, client_index); } } + ipc_send_completed_request_replies(self); + ipc_fail_pending_requests(self); + ipc_flush_clients_blocking(self); return NULL; } @@ -403,6 +771,11 @@ static void ipc_close(gsr_ipc *self) { } } + 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; @@ -412,11 +785,17 @@ static void ipc_close(gsr_ipc *self) { 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; @@ -430,6 +809,12 @@ int gsr_ipc_init(gsr_ipc *self, const char *socket_filepath) { 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; @@ -437,7 +822,7 @@ int gsr_ipc_init(gsr_ipc *self, const char *socket_filepath) { goto err; } - if(!fd_set_cloexec(self->wakeup_pipe[0]) || !fd_set_cloexec(self->wakeup_pipe[1])) { + 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; } @@ -448,6 +833,14 @@ int gsr_ipc_init(gsr_ipc *self, const char *socket_filepath) { 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; @@ -500,15 +893,27 @@ void gsr_ipc_stop(gsr_ipc *self) { if(!self->thread_running) return; - const char wakeup_value = 1; - 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_stop: failed to wake up the ipc thread, error: %s", strerror(errno)); - + 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 index d34e8cc..de014a7 100644 --- a/src/cli/main.c +++ b/src/cli/main.c @@ -137,6 +137,17 @@ static bool ipc_toggle_pause_handler(char *error_message, size_t error_message_s 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) { @@ -148,6 +159,33 @@ static bool ipc_toggle_replay_recording_handler(char *error_message, size_t erro 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, char *error_message, size_t error_message_size, void *userdata) { const gsr_recorder_settings *settings = userdata; if(!settings->is_replaying) { @@ -295,18 +333,26 @@ static void screenshot_saved_callback(const char *filepath, void *userdata) { 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) { - const char *recording_saved_script = 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(recording_saved_script) - run_recording_saved_script_async(recording_saved_script, filepath, "replay"); + 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) { @@ -318,17 +364,20 @@ static void recording_started_callback(const char *filepath, void *userdata) { } static void recording_stopped_callback(const char *filepath, void *userdata) { - const char *recording_saved_script = 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(recording_saved_script) - run_recording_saved_script_async(recording_saved_script, filepath, "regular"); + 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 @@ -410,12 +459,16 @@ static int record(args_parser *arg_parser, gsr_windowing *windowing, gsr_capture 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 = (void*)arg_parser->settings.recording_saved_script; + callbacks.userdata = &callbacks_context; int error = GSR_ERROR_OK; recorder = gsr_recorder_create(&recorder_params, &callbacks, &error); @@ -427,9 +480,13 @@ static int record(args_parser *arg_parser, gsr_windowing *windowing, gsr_capture 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; @@ -437,6 +494,7 @@ static int record(args_parser *arg_parser, gsr_windowing *windowing, gsr_capture 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); recorder = NULL; diff --git a/src/recorder/recorder.c b/src/recorder/recorder.c index 4426ac5..4bffb8e 100644 --- a/src/recorder/recorder.c +++ b/src/recorder/recorder.c @@ -35,6 +35,15 @@ #define GSR_VIDEO_STREAM_INDEX 0 +#define GSR_SET_PAUSED_REQUEST_NONE -1 +#define GSR_SET_PAUSED_REQUEST_UNPAUSE 0 +#define GSR_SET_PAUSED_REQUEST_PAUSE 1 + +#define GSR_REPLAY_RECORDING_REQUEST_NONE 0 +#define GSR_REPLAY_RECORDING_REQUEST_TOGGLE 1 +#define GSR_REPLAY_RECORDING_REQUEST_START 2 +#define GSR_REPLAY_RECORDING_REQUEST_STOP 3 + struct gsr_recorder { gsr_recorder_settings settings; gsr_recorder_callbacks callbacks; @@ -88,7 +97,9 @@ struct gsr_recorder { atomic_int running; atomic_int toggle_pause; - atomic_int toggle_replay_recording; + atomic_int set_paused_request; + atomic_int replay_recording_request; + atomic_int replay_recording_state; atomic_int save_replay_seconds; bool should_stop_error; bool force_iframe_frame; @@ -403,7 +414,9 @@ gsr_recorder* gsr_recorder_create(const gsr_recorder_params *params, const gsr_r self->audio_input_tracks = params->audio_input_tracks; atomic_init(&self->running, 1); atomic_init(&self->toggle_pause, 0); - atomic_init(&self->toggle_replay_recording, 0); + atomic_init(&self->set_paused_request, GSR_SET_PAUSED_REQUEST_NONE); + atomic_init(&self->replay_recording_request, GSR_REPLAY_RECORDING_REQUEST_NONE); + atomic_init(&self->replay_recording_state, 0); atomic_init(&self->save_replay_seconds, 0); self->audio_max_frame_size = 1024; int error_code = GSR_ERROR_GENERIC; @@ -625,78 +638,99 @@ static void recorder_capture_and_encode_frame(gsr_recorder *self, bool damaged) } static void recorder_apply_pause_toggle(gsr_recorder *self) { - if(atomic_load(&self->toggle_pause) == 1 && !self->settings.is_replaying) { - self->paused = !self->paused; + const bool toggle_pause = atomic_exchange(&self->toggle_pause, 0) == 1; + const int set_paused_request = atomic_exchange(&self->set_paused_request, GSR_SET_PAUSED_REQUEST_NONE); + if(self->settings.is_replaying) + return; + + bool new_paused = self->paused; + if(toggle_pause) + new_paused = !new_paused; + if(set_paused_request != GSR_SET_PAUSED_REQUEST_NONE) + new_paused = set_paused_request == GSR_SET_PAUSED_REQUEST_PAUSE; + + if(new_paused != self->paused) { + self->paused = new_paused; gsr_recording_clock_set_paused(self->recording_clock, self->paused); gsr_log(GSR_LOG_LEVEL_INFO, self->paused ? "Paused" : "Unpaused"); - atomic_store(&self->toggle_pause, 0); } } static void recorder_apply_replay_recording_toggle(gsr_recorder *self) { - if(atomic_load(&self->toggle_replay_recording) && !self->settings.replay_recording_directory) { - atomic_store(&self->toggle_replay_recording, 0); - if(self->callbacks.recording_started) + const int request = atomic_exchange(&self->replay_recording_request, GSR_REPLAY_RECORDING_REQUEST_NONE); + if(request == GSR_REPLAY_RECORDING_REQUEST_NONE) + return; + + if(!self->settings.replay_recording_directory) { + if(request != GSR_REPLAY_RECORDING_REQUEST_STOP && self->callbacks.recording_started) self->callbacks.recording_started(NULL, self->callbacks.userdata); + return; } - if(atomic_load(&self->toggle_replay_recording) && self->settings.replay_recording_directory) { - atomic_store(&self->toggle_replay_recording, 0); - const bool new_replay_recording_state = !self->replay_recording; - if(new_replay_recording_state) { - gsr_audio_capture_lock_filter(&self->audio_capture); - self->num_replay_recording_items = 0; - const bool filepath_created = gsr_create_new_recording_filepath_from_timestamp(self->replay_recording_filepath, sizeof(self->replay_recording_filepath), self->settings.replay_recording_directory, "Video", self->file_extension, self->settings.date_folders); - if(filepath_created && gsr_recording_output_start(&self->replay_recording_output, self->replay_recording_filepath, &self->settings, self->video_codec_context, &self->audio_capture, self->hdr, self->video_sources)) { - const size_t video_recording_destination_id = gsr_encoder_add_recording_destination(&self->encoder, self->video_codec_context, self->replay_recording_output.av_format_context, self->replay_recording_output.video_stream, self->video_frame->pts); - if(self->settings.write_first_frame_ts && video_recording_destination_id != (size_t)-1) { - char ts_filepath[PATH_MAX + 4]; - snprintf(ts_filepath, sizeof(ts_filepath), "%s.ts", self->replay_recording_filepath); - gsr_encoder_set_recording_destination_first_frame_ts_filepath(&self->encoder, video_recording_destination_id, ts_filepath); - } - - if(video_recording_destination_id != (size_t)-1 && self->num_replay_recording_items < GSR_MAX_RECORDING_DESTINATIONS) { - self->replay_recording_items[self->num_replay_recording_items] = video_recording_destination_id; - ++self->num_replay_recording_items; - } + bool new_replay_recording_state = !self->replay_recording; + if(request == GSR_REPLAY_RECORDING_REQUEST_START) + new_replay_recording_state = true; + else if(request == GSR_REPLAY_RECORDING_REQUEST_STOP) + new_replay_recording_state = false; - for(size_t i = 0; i < self->replay_recording_output.num_audio_streams; ++i) { - const gsr_recording_audio_stream *audio_stream = &self->replay_recording_output.audio_streams[i]; - const size_t audio_recording_destination_id = gsr_encoder_add_recording_destination(&self->encoder, audio_stream->audio_track->codec_context, self->replay_recording_output.av_format_context, audio_stream->stream, audio_stream->audio_track->pts); - if(audio_recording_destination_id != (size_t)-1 && self->num_replay_recording_items < GSR_MAX_RECORDING_DESTINATIONS) { - self->replay_recording_items[self->num_replay_recording_items] = audio_recording_destination_id; - ++self->num_replay_recording_items; - } - } + if(new_replay_recording_state == self->replay_recording) + return; - self->replay_recording = true; - self->force_iframe_frame = true; - gsr_log(GSR_LOG_LEVEL_INFO, "Started recording"); - if(self->callbacks.recording_started) - self->callbacks.recording_started(self->replay_recording_filepath, self->callbacks.userdata); - } else { - if(self->callbacks.recording_started) - self->callbacks.recording_started(NULL, self->callbacks.userdata); + if(new_replay_recording_state) { + gsr_audio_capture_lock_filter(&self->audio_capture); + self->num_replay_recording_items = 0; + const bool filepath_created = gsr_create_new_recording_filepath_from_timestamp(self->replay_recording_filepath, sizeof(self->replay_recording_filepath), self->settings.replay_recording_directory, "Video", self->file_extension, self->settings.date_folders); + if(filepath_created && gsr_recording_output_start(&self->replay_recording_output, self->replay_recording_filepath, &self->settings, self->video_codec_context, &self->audio_capture, self->hdr, self->video_sources)) { + const size_t video_recording_destination_id = gsr_encoder_add_recording_destination(&self->encoder, self->video_codec_context, self->replay_recording_output.av_format_context, self->replay_recording_output.video_stream, self->video_frame->pts); + if(self->settings.write_first_frame_ts && video_recording_destination_id != (size_t)-1) { + char ts_filepath[PATH_MAX + 4]; + snprintf(ts_filepath, sizeof(ts_filepath), "%s.ts", self->replay_recording_filepath); + gsr_encoder_set_recording_destination_first_frame_ts_filepath(&self->encoder, video_recording_destination_id, ts_filepath); } - gsr_audio_capture_unlock_filter(&self->audio_capture); - } else if(self->replay_recording_output.av_format_context) { - for(size_t i = 0; i < self->num_replay_recording_items; ++i) { - gsr_encoder_remove_recording_destination(&self->encoder, self->replay_recording_items[i]); + + if(video_recording_destination_id != (size_t)-1 && self->num_replay_recording_items < GSR_MAX_RECORDING_DESTINATIONS) { + self->replay_recording_items[self->num_replay_recording_items] = video_recording_destination_id; + ++self->num_replay_recording_items; } - self->num_replay_recording_items = 0; - if(gsr_recording_output_stop(&self->replay_recording_output)) { - gsr_log(GSR_LOG_LEVEL_INFO, "Stopped recording"); - if(self->callbacks.recording_stopped) - self->callbacks.recording_stopped(self->replay_recording_filepath, self->callbacks.userdata); - } else { - if(self->callbacks.recording_stopped) - self->callbacks.recording_stopped(NULL, self->callbacks.userdata); + for(size_t i = 0; i < self->replay_recording_output.num_audio_streams; ++i) { + const gsr_recording_audio_stream *audio_stream = &self->replay_recording_output.audio_streams[i]; + const size_t audio_recording_destination_id = gsr_encoder_add_recording_destination(&self->encoder, audio_stream->audio_track->codec_context, self->replay_recording_output.av_format_context, audio_stream->stream, audio_stream->audio_track->pts); + if(audio_recording_destination_id != (size_t)-1 && self->num_replay_recording_items < GSR_MAX_RECORDING_DESTINATIONS) { + self->replay_recording_items[self->num_replay_recording_items] = audio_recording_destination_id; + ++self->num_replay_recording_items; + } } - self->replay_recording = false; - self->replay_recording_filepath[0] = '\0'; + self->replay_recording = true; + atomic_store(&self->replay_recording_state, 1); + self->force_iframe_frame = true; + gsr_log(GSR_LOG_LEVEL_INFO, "Started recording"); + if(self->callbacks.recording_started) + self->callbacks.recording_started(self->replay_recording_filepath, self->callbacks.userdata); + } else { + if(self->callbacks.recording_started) + self->callbacks.recording_started(NULL, self->callbacks.userdata); } + gsr_audio_capture_unlock_filter(&self->audio_capture); + } else if(self->replay_recording_output.av_format_context) { + for(size_t i = 0; i < self->num_replay_recording_items; ++i) { + gsr_encoder_remove_recording_destination(&self->encoder, self->replay_recording_items[i]); + } + self->num_replay_recording_items = 0; + + if(gsr_recording_output_stop(&self->replay_recording_output)) { + gsr_log(GSR_LOG_LEVEL_INFO, "Stopped recording"); + if(self->callbacks.recording_stopped) + self->callbacks.recording_stopped(self->replay_recording_filepath, self->callbacks.userdata); + } else { + if(self->callbacks.recording_stopped) + self->callbacks.recording_stopped(NULL, self->callbacks.userdata); + } + + self->replay_recording = false; + atomic_store(&self->replay_recording_state, 0); + self->replay_recording_filepath[0] = '\0'; } } @@ -905,8 +939,24 @@ void gsr_recorder_toggle_pause(gsr_recorder *self) { atomic_store(&self->toggle_pause, 1); } +void gsr_recorder_set_paused(gsr_recorder *self, bool paused) { + atomic_store(&self->set_paused_request, paused ? GSR_SET_PAUSED_REQUEST_PAUSE : GSR_SET_PAUSED_REQUEST_UNPAUSE); +} + void gsr_recorder_toggle_replay_recording(gsr_recorder *self) { - atomic_store(&self->toggle_replay_recording, 1); + atomic_store(&self->replay_recording_request, GSR_REPLAY_RECORDING_REQUEST_TOGGLE); +} + +void gsr_recorder_start_replay_recording(gsr_recorder *self) { + atomic_store(&self->replay_recording_request, GSR_REPLAY_RECORDING_REQUEST_START); +} + +void gsr_recorder_stop_replay_recording(gsr_recorder *self) { + atomic_store(&self->replay_recording_request, GSR_REPLAY_RECORDING_REQUEST_STOP); +} + +bool gsr_recorder_is_replay_recording(const gsr_recorder *self) { + return atomic_load(&self->replay_recording_state) == 1; } void gsr_recorder_save_replay(gsr_recorder *self, int seconds) { diff --git a/src/recorder/replay_save.c b/src/recorder/replay_save.c index 1d33437..c3a1bea 100644 --- a/src/recorder/replay_save.c +++ b/src/recorder/replay_save.c @@ -140,7 +140,7 @@ bool gsr_replay_save_start(gsr_replay_save *self, AVCodecContext *video_codec_co if(self->video_start_iterator.packet_index == (size_t)-1) { gsr_log(GSR_LOG_LEVEL_ERROR, "failed to save replay: failed to find a video keyframe. perhaps replay was saved too fast, before anything has been recorded"); gsr_replay_save_cleanup(self); - return true; + return false; } self->video_pts_offset = gsr_replay_buffer_iterator_get_packet(self->cloned_replay_buffer, self->video_start_iterator)->pts; diff --git a/tools/gsr-cli/main.c b/tools/gsr-cli/main.c index b536ceb..e430964 100644 --- a/tools/gsr-cli/main.c +++ b/tools/gsr-cli/main.c @@ -14,8 +14,9 @@ #define GSR_CLI_REQUEST_ID 1 #define GSR_CLI_MAX_REQUEST_SIZE 256 -#define GSR_CLI_MAX_REPLY_SIZE 4096 +#define GSR_CLI_MAX_REPLY_SIZE 8192 #define GSR_CLI_REPLY_TIMEOUT_SECONDS 10 +#define GSR_CLI_NO_REPLY_TIMEOUT 0 static void usage(void) { printf("usage: gsr-cli -ipc [command_argument]\n"); @@ -31,14 +32,24 @@ static void usage(void) { printf(" Check if a GPU Screen Recorder instance is listening on the socket. Prints \"running\" or\n"); printf(" \"not running\" and exits with 0 when it's running.\n"); printf(" stop\n"); - printf(" Stop and save the recording (stop without save in replay mode).\n"); + printf(" Stop and save the recording (stop without save in replay mode). Waits until the recording\n"); + printf(" has been saved and prints the path of the saved file.\n"); printf(" toggle-pause\n"); printf(" Pause/unpause the recording (not for streaming/replay).\n"); + printf(" set-paused true|false\n"); + printf(" Pause/unpause the recording (not for streaming/replay). Unlike toggle-pause this doesn't\n"); + printf(" fail when the recording is already paused/unpaused.\n"); printf(" toggle-replay-recording\n"); printf(" Start/stop a regular recording during replay/streaming.\n"); + printf(" start-replay-recording\n"); + printf(" Start a regular recording during replay/streaming. Does nothing when a recording is already running.\n"); + printf(" stop-replay-recording\n"); + printf(" Stop the regular recording that runs during replay/streaming. Waits until the recording\n"); + printf(" has been saved and prints the path of the saved file.\n"); printf(" save-replay [seconds]\n"); printf(" Save the replay. The number of seconds has to be larger than 0. The whole replay buffer is\n"); - printf(" saved when no number of seconds is given.\n"); + printf(" saved when no number of seconds is given. Waits until the replay has been saved and prints\n"); + printf(" the path of the saved file.\n"); printf("\n"); printf("EXAMPLES:\n"); printf(" gsr-cli -ipc \"$XDG_RUNTIME_DIR/gsr.sock\" status\n"); @@ -58,7 +69,7 @@ static bool string_to_int64(const char *str, int64_t *result) { } /* Returns the socket, or -1 on failure. Only logs an error when the failure isn't a missing GPU Screen Recorder instance */ -static int ipc_connect(const char *socket_filepath) { +static int ipc_connect(const char *socket_filepath, int reply_timeout_seconds) { struct sockaddr_un addr; memset(&addr, 0, sizeof(addr)); addr.sun_family = AF_UNIX; @@ -74,9 +85,12 @@ static int ipc_connect(const char *socket_filepath) { } struct timeval timeout; - timeout.tv_sec = GSR_CLI_REPLY_TIMEOUT_SECONDS; + timeout.tv_sec = reply_timeout_seconds; timeout.tv_usec = 0; - setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout)); + if(reply_timeout_seconds != GSR_CLI_NO_REPLY_TIMEOUT) + setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout)); + + timeout.tv_sec = GSR_CLI_REPLY_TIMEOUT_SECONDS; setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &timeout, sizeof(timeout)); if(connect(fd, (const struct sockaddr*)&addr, sizeof(addr)) == -1) { @@ -140,6 +154,23 @@ static bool ipc_receive_reply(int fd, char *reply, size_t reply_capacity, size_t } } +static void print_json_string(const char *str, size_t size) { + for(size_t i = 0; i < size; ++i) { + char c = str[i]; + if(c == '\\' && i + 1 < size) { + ++i; + switch(str[i]) { + case 'n': c = '\n'; break; + case 'r': c = '\r'; break; + case 't': c = '\t'; break; + default: c = str[i]; break; + } + } + putchar(c); + } + putchar('\n'); +} + /* Returns the exit code that gsr-cli should exit with */ static int ipc_handle_reply(char *reply, size_t reply_size, int64_t request_id) { sj_Reader reader = sj_reader(reply, reply_size); @@ -185,8 +216,11 @@ static int ipc_handle_reply(char *reply, size_t reply_size, int64_t request_id) return 1; } - if(gsr_json_string_equals(&result_value, "ok")) + if(gsr_json_string_equals(&result_value, "ok")) { + if(has_data && data_value.type == SJ_STRING) + print_json_string(data_value.start, data_value.end - data_value.start); return 0; + } if(has_data && data_value.type == SJ_STRING) gsr_log(GSR_LOG_LEVEL_ERROR, "%.*s", (int)(data_value.end - data_value.start), data_value.start); @@ -197,7 +231,7 @@ static int ipc_handle_reply(char *reply, size_t reply_size, int64_t request_id) } static int status_command(const char *socket_filepath) { - const int fd = ipc_connect(socket_filepath); + const int fd = ipc_connect(socket_filepath, GSR_CLI_REPLY_TIMEOUT_SECONDS); if(fd == -1) { printf("not running\n"); fflush(stdout); @@ -210,20 +244,8 @@ static int status_command(const char *socket_filepath) { return 0; } -static int send_command(const char *socket_filepath, const char *name, const char *seconds_str) { - char request[GSR_CLI_MAX_REQUEST_SIZE]; - if(seconds_str) { - int64_t seconds = 0; - if(!string_to_int64(seconds_str, &seconds) || seconds <= 0 || seconds > INT_MAX) { - gsr_log(GSR_LOG_LEVEL_ERROR, "expected the number of seconds to save to be an integer larger than 0, got: '%s'", seconds_str); - return 1; - } - snprintf(request, sizeof(request), "{\"id\":%d,\"name\":\"%s\",\"data\":%" PRIi64 "}\n", GSR_CLI_REQUEST_ID, name, seconds); - } else { - snprintf(request, sizeof(request), "{\"id\":%d,\"name\":\"%s\"}\n", GSR_CLI_REQUEST_ID, name); - } - - const int fd = ipc_connect(socket_filepath); +static int send_request(const char *socket_filepath, const char *request, int reply_timeout_seconds) { + const int fd = ipc_connect(socket_filepath, reply_timeout_seconds); if(fd == -1) { gsr_log(GSR_LOG_LEVEL_ERROR, "failed to connect to \"%s\". Is GPU Screen Recorder running with the -ipc option?", socket_filepath); return 1; @@ -259,9 +281,31 @@ int main(int argc, char **argv) { const char *socket_filepath = argv[2]; const char *command = argv[3]; const char *command_argument = argc == 5 ? argv[4] : NULL; + char request[GSR_CLI_MAX_REQUEST_SIZE]; - if(strcmp(command, "save-replay") == 0) - return send_command(socket_filepath, command, command_argument); + if(strcmp(command, "save-replay") == 0) { + if(command_argument) { + int64_t seconds = 0; + if(!string_to_int64(command_argument, &seconds) || seconds <= 0 || seconds > INT_MAX) { + gsr_log(GSR_LOG_LEVEL_ERROR, "expected the number of seconds to save to be an integer larger than 0, got: '%s'", command_argument); + return 1; + } + snprintf(request, sizeof(request), "{\"id\":%d,\"name\":\"save-replay\",\"data\":%" PRIi64 "}\n", GSR_CLI_REQUEST_ID, seconds); + } else { + snprintf(request, sizeof(request), "{\"id\":%d,\"name\":\"save-replay\"}\n", GSR_CLI_REQUEST_ID); + } + return send_request(socket_filepath, request, GSR_CLI_NO_REPLY_TIMEOUT); + } + + if(strcmp(command, "set-paused") == 0) { + if(!command_argument || (strcmp(command_argument, "true") != 0 && strcmp(command_argument, "false") != 0)) { + gsr_log(GSR_LOG_LEVEL_ERROR, "the 'set-paused' command expects either true or false as the argument"); + usage(); + return 1; + } + snprintf(request, sizeof(request), "{\"id\":%d,\"name\":\"set-paused\",\"data\":%s}\n", GSR_CLI_REQUEST_ID, command_argument); + return send_request(socket_filepath, request, GSR_CLI_REPLY_TIMEOUT_SECONDS); + } if(command_argument) { gsr_log(GSR_LOG_LEVEL_ERROR, "the '%s' command doesn't take an argument", command); @@ -272,8 +316,15 @@ int main(int argc, char **argv) { if(strcmp(command, "status") == 0) return status_command(socket_filepath); - if(strcmp(command, "stop") == 0 || strcmp(command, "toggle-pause") == 0 || strcmp(command, "toggle-replay-recording") == 0) - return send_command(socket_filepath, command, NULL); + if(strcmp(command, "toggle-pause") == 0 || strcmp(command, "toggle-replay-recording") == 0 || strcmp(command, "start-replay-recording") == 0) { + snprintf(request, sizeof(request), "{\"id\":%d,\"name\":\"%s\"}\n", GSR_CLI_REQUEST_ID, command); + return send_request(socket_filepath, request, GSR_CLI_REPLY_TIMEOUT_SECONDS); + } + + if(strcmp(command, "stop") == 0 || strcmp(command, "stop-replay-recording") == 0) { + snprintf(request, sizeof(request), "{\"id\":%d,\"name\":\"%s\"}\n", GSR_CLI_REQUEST_ID, command); + return send_request(socket_filepath, request, GSR_CLI_NO_REPLY_TIMEOUT); + } gsr_log(GSR_LOG_LEVEL_ERROR, "invalid command '%s'", command); usage(); -- cgit v1.2.3 From e6c1cf0ec1ce220912a6ef6b6fac5ba9b2e98276 Mon Sep 17 00:00:00 2001 From: dec05eba Date: Wed, 5 Aug 2026 14:56:59 +0200 Subject: workaround ffmpeg hybrid_fragmented bug when no packets have been recorded yet --- include/ffmpeg_utils.h | 13 ++++++++++ src/encoder/encoder.c | 3 +++ src/ffmpeg_utils.c | 28 ++++++++++++++++++++++ src/recorder/muxer.c | 2 +- src/recorder/recorder.c | 2 +- src/recorder/replay_save.c | 4 +++- .../ffmpeg-mbedtls-default-ca-certs.patch | 2 +- 7 files changed, 50 insertions(+), 4 deletions(-) diff --git a/include/ffmpeg_utils.h b/include/ffmpeg_utils.h index 35524c2..6d7481b 100644 --- a/include/ffmpeg_utils.h +++ b/include/ffmpeg_utils.h @@ -1,6 +1,19 @@ #ifndef GSR_FFMPEG_UTILS_H #define GSR_FFMPEG_UTILS_H +#include + +typedef struct AVFormatContext AVFormatContext; + const char* gsr_av_error_to_string(int err); +/* Marks that a packet has been successfully written to |av_format_context|, see gsr_av_format_context_write_trailer */ +void gsr_av_format_context_mark_packet_written(AVFormatContext *av_format_context); +/* + The same as av_write_trailer, except that the trailer is not written when no packet has been written to a muxer + that uses the hybrid_fragmented movflags option (see set_format_context_options), because the mov muxer in FFmpeg + crashes when it finalizes a hybrid_fragmented file that has no packets. Returns 0 on success, just like av_write_trailer. +*/ +int gsr_av_format_context_write_trailer(AVFormatContext *av_format_context); + #endif /* GSR_FFMPEG_UTILS_H */ diff --git a/src/encoder/encoder.c b/src/encoder/encoder.c index 2df4666..3aebb7a 100644 --- a/src/encoder/encoder.c +++ b/src/encoder/encoder.c @@ -1,6 +1,7 @@ #include "../../include/encoder/encoder.h" #include "../../include/log.h" #include "../../include/utils.h" +#include "../../include/ffmpeg_utils.h" #include #include @@ -152,6 +153,8 @@ void gsr_encoder_receive_packets(gsr_encoder *self, AVCodecContext *codec_contex // TODO: Is av_interleaved_write_frame needed?. Answer: might be needed for mkv but dont use it! it causes frames to be inconsistent, skipping frames and duplicating frames. // TODO: av_interleaved_write_frame might be needed for cfr, or always for flv const int ret = av_write_frame(recording_destination->format_context, av_packet); + if(ret >= 0) + gsr_av_format_context_mark_packet_written(recording_destination->format_context); if(ret < 0) { char error_buffer[AV_ERROR_MAX_STRING_SIZE]; if(av_strerror(ret, error_buffer, sizeof(error_buffer)) < 0) diff --git a/src/ffmpeg_utils.c b/src/ffmpeg_utils.c index ce239a1..fc4e4ed 100644 --- a/src/ffmpeg_utils.c +++ b/src/ffmpeg_utils.c @@ -1,7 +1,10 @@ #include "../include/ffmpeg_utils.h" +#include "../include/log.h" #include #include +#include +#include static _Thread_local char av_error_buffer[AV_ERROR_MAX_STRING_SIZE]; @@ -10,3 +13,28 @@ const char* gsr_av_error_to_string(int err) { strcpy(av_error_buffer, "Unknown error"); return av_error_buffer; } + +void gsr_av_format_context_mark_packet_written(AVFormatContext *av_format_context) { + av_format_context->opaque = (void*)1; +} + +static bool av_format_context_uses_hybrid_fragmented(AVFormatContext *av_format_context) { + if(LIBAVFORMAT_VERSION_INT < AV_VERSION_INT(62, 6, 101)) + return false; + + const AVOption *opt = av_opt_find(av_format_context->priv_data, "movflags", NULL, 0, 0); + if(!opt || !opt->unit) + return false; + + return av_opt_find(av_format_context->priv_data, "hybrid_fragmented", opt->unit, 0, 0) != NULL; +} + +int gsr_av_format_context_write_trailer(AVFormatContext *av_format_context) { + const bool packet_written = av_format_context->opaque != NULL; + if(!packet_written && av_format_context_uses_hybrid_fragmented(av_format_context)) { + gsr_log(GSR_LOG_LEVEL_WARNING, "not finalizing the video file because it has no video/audio data"); + return 0; + } + + return av_write_trailer(av_format_context); +} diff --git a/src/recorder/muxer.c b/src/recorder/muxer.c index ff4ec06..806921d 100644 --- a/src/recorder/muxer.c +++ b/src/recorder/muxer.c @@ -197,7 +197,7 @@ bool gsr_recording_output_start(gsr_recording_output *self, const char *filename bool gsr_recording_output_stop(gsr_recording_output *self) { bool trailer_written = true; - if(av_write_trailer(self->av_format_context) != 0) { + if(gsr_av_format_context_write_trailer(self->av_format_context) != 0) { //trailer_written = false; } diff --git a/src/recorder/recorder.c b/src/recorder/recorder.c index 4bffb8e..fc0ccf0 100644 --- a/src/recorder/recorder.c +++ b/src/recorder/recorder.c @@ -886,7 +886,7 @@ static void gsr_recorder_stop_recording(gsr_recorder *self) { gsr_audio_capture_join_threads(&self->audio_capture); // TODO: Replace this with start_recording_create_steams - if(!self->settings.is_replaying && av_write_trailer(self->av_format_context) != 0) { + if(!self->settings.is_replaying && gsr_av_format_context_write_trailer(self->av_format_context) != 0) { //fprintf(stderr, "Failed to write trailer\n"); } diff --git a/src/recorder/replay_save.c b/src/recorder/replay_save.c index c3a1bea..3251616 100644 --- a/src/recorder/replay_save.c +++ b/src/recorder/replay_save.c @@ -96,7 +96,9 @@ static void* replay_save_thread(void *userdata) { av_packet_rescale_ts(&av_packet, codec_context->time_base, stream->time_base); const int ret = av_write_frame(self->recording_output.av_format_context, &av_packet); - if(ret < 0) + if(ret >= 0) + gsr_av_format_context_mark_packet_written(self->recording_output.av_format_context); + else gsr_log(GSR_LOG_LEVEL_ERROR, "Failed to write frame index %d to muxer, reason: %s (%d)", av_packet.stream_index, gsr_av_error_to_string(ret), ret); free(replay_packet_data); diff --git a/subprojects/packagefiles/ffmpeg-mbedtls-default-ca-certs.patch b/subprojects/packagefiles/ffmpeg-mbedtls-default-ca-certs.patch index a22d4ae..2a36992 100644 --- a/subprojects/packagefiles/ffmpeg-mbedtls-default-ca-certs.patch +++ b/subprojects/packagefiles/ffmpeg-mbedtls-default-ca-certs.patch @@ -1,6 +1,6 @@ --- a/libavformat/tls_mbedtls.c +++ b/libavformat/tls_mbedtls.c -@@ -42,6 +42,66 @@ +@@ -42,6 +42,64 @@ #include "libavutil/avstring.h" #include "libavutil/random_seed.h" #include "libavutil/intreadwrite.h" -- cgit v1.2.3 From f17157274d02ddf88af7af95ad6353b231983f5a Mon Sep 17 00:00:00 2001 From: dec05eba Date: Wed, 5 Aug 2026 15:04:38 +0200 Subject: Re-add /etc/ssl/certs/ca-certificates.crt to mbedtls patch --- subprojects/packagefiles/ffmpeg-mbedtls-default-ca-certs.patch | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/subprojects/packagefiles/ffmpeg-mbedtls-default-ca-certs.patch b/subprojects/packagefiles/ffmpeg-mbedtls-default-ca-certs.patch index 2a36992..c2c4c3f 100644 --- a/subprojects/packagefiles/ffmpeg-mbedtls-default-ca-certs.patch +++ b/subprojects/packagefiles/ffmpeg-mbedtls-default-ca-certs.patch @@ -1,6 +1,6 @@ --- a/libavformat/tls_mbedtls.c +++ b/libavformat/tls_mbedtls.c -@@ -42,6 +42,64 @@ +@@ -42,6 +42,65 @@ #include "libavutil/avstring.h" #include "libavutil/random_seed.h" #include "libavutil/intreadwrite.h" @@ -14,6 +14,7 @@ + * environment variables as openssl does. + */ +static const char * const default_ca_files[] = { ++ "/etc/ssl/certs/ca-certificates.crt", // debian, ubuntu, arch, alpine, gentoo + "/etc/ssl/ca-bundle.pem", // opensuse + "/etc/ssl/cert.pem", // openbsd, freebsd, macos + "/usr/local/etc/ssl/cert.pem", // freebsd ports -- cgit v1.2.3 From 3a1ce0e17f209702095aaa86fedeb74400b3d039 Mon Sep 17 00:00:00 2001 From: dec05eba Date: Wed, 5 Aug 2026 15:26:36 +0200 Subject: ipc: save-replay: add restart-replay option --- gpu-screen-recorder.1 | 18 ++++++++++++-- gsr-cli.1 | 16 +++++++++++-- include/cli/ipc.h | 7 ++++-- include/recorder/recorder.h | 11 +++++++-- src/cli/ipc.c | 57 +++++++++++++++++++++++++++++++++++++++------ src/cli/main.c | 12 ++++++---- src/recorder/recorder.c | 9 +++++-- tools/gsr-cli/main.c | 56 ++++++++++++++++++++++++++++++++++---------- 8 files changed, 153 insertions(+), 33 deletions(-) diff --git a/gpu-screen-recorder.1 b/gpu-screen-recorder.1 index 67857a8..9f461cc 100644 --- a/gpu-screen-recorder.1 +++ b/gpu-screen-recorder.1 @@ -527,8 +527,22 @@ saved file, except in replay mode where nothing is saved. .B save-replay Save replay (replay mode only). .B data -is the number of seconds to save, which has to be larger than 0. The whole replay buffer is saved when +is an object with these optional fields: +.RS +.TP +.B seconds +The number of seconds to save, which has to be larger than 0. +.TP +.B restart-replay +true/false, which overrides the +.B \-restart\-replay\-on\-save +option for this save. Just like that option, the replay buffer is only cleared when the whole replay buffer is saved. +.RE +.IP +The whole replay buffer is saved when .B data +is omitted or null, or when +.B seconds is omitted or null. The reply contains the path of the saved file. .TP .B toggle-pause @@ -559,7 +573,7 @@ Example: .nf .RS gpu-screen-recorder -w screen -f 60 -c mp4 -r 60 -o ~/Videos -ipc "$XDG_RUNTIME_DIR/gsr.sock" & -echo '{"id":1,"name":"save-replay","data":30}' | socat - "UNIX-CONNECT:$XDG_RUNTIME_DIR/gsr.sock" +echo '{"id":1,"name":"save-replay","data":{"seconds":30}}' | socat - "UNIX-CONNECT:$XDG_RUNTIME_DIR/gsr.sock" .RE .fi which replies with the following when the replay has been saved: diff --git a/gsr-cli.1 b/gsr-cli.1 index 0a05df9..14d1f51 100644 --- a/gsr-cli.1 +++ b/gsr-cli.1 @@ -6,7 +6,7 @@ gsr\-cli \- Control a running GPU Screen Recorder instance .B \-ipc .I socket_path .I command -.RI [ command_argument ] +.RI [ command_arguments... ] .PP .B gsr\-cli .B \-h @@ -70,10 +70,15 @@ option. Does nothing when a recording is already running. Stop the regular recording that runs during replay/streaming. Waits until the recording has been saved and prints the path of the saved file. Fails when no recording is running. .TP -.BR save-replay " [" \fIseconds\fR ] +.BR save-replay " [" \fIseconds\fR "] [" restart-replay=true|false ] Save replay (replay mode only). The number of seconds has to be larger than 0. The whole replay buffer is saved when no number of seconds is given. Waits until the replay has been saved and prints the path of the saved file. +.B restart-replay +overrides the +.B \-restart\-replay\-on\-save +option of GPU Screen Recorder for this save, which clears the replay buffer after the whole replay +buffer has been saved. .SH EXAMPLES Start GPU Screen Recorder in replay mode with an ipc socket: .RS @@ -89,6 +94,13 @@ gsr-cli -ipc "$XDG_RUNTIME_DIR/gsr.sock" save-replay 30 .fi .RE .PP +Save the whole replay buffer and clear it: +.RS +.nf +gsr-cli -ipc "$XDG_RUNTIME_DIR/gsr.sock" save-replay restart-replay=true +.fi +.RE +.PP Start replay only when it isn't already running: .RS .nf diff --git a/include/cli/ipc.h b/include/cli/ipc.h index 6008db9..0c4c076 100644 --- a/include/cli/ipc.h +++ b/include/cli/ipc.h @@ -28,8 +28,11 @@ typedef struct { bool (*toggle_replay_recording)(char *error_message, size_t error_message_size, void *userdata); bool (*start_replay_recording)(char *error_message, size_t error_message_size, void *userdata); bool (*stop_replay_recording)(char *error_message, size_t error_message_size, void *userdata); - /* |seconds| is GSR_SAVE_REPLAY_SECONDS_FULL when the whole replay buffer should be saved */ - bool (*save_replay)(int seconds, char *error_message, size_t error_message_size, void *userdata); + /* + |seconds| is GSR_SAVE_REPLAY_SECONDS_FULL when the whole replay buffer should be saved. + |restart_replay| overrides the -restart-replay-on-save option for this save when |has_restart_replay| is set. + */ + bool (*save_replay)(int seconds, bool has_restart_replay, bool restart_replay, char *error_message, size_t error_message_size, void *userdata); void *userdata; } gsr_ipc_handlers; diff --git a/include/recorder/recorder.h b/include/recorder/recorder.h index 887b4fe..5721a98 100644 --- a/include/recorder/recorder.h +++ b/include/recorder/recorder.h @@ -55,7 +55,14 @@ void gsr_recorder_start_replay_recording(gsr_recorder *self); /* Does nothing when no recording is running */ void gsr_recorder_stop_replay_recording(gsr_recorder *self); bool gsr_recorder_is_replay_recording(const gsr_recorder *self); -/* |seconds| can be GSR_SAVE_REPLAY_SECONDS_FULL to save the whole replay buffer */ -void gsr_recorder_save_replay(gsr_recorder *self, int seconds); +#define GSR_RESTART_REPLAY_USE_OPTION -1 +#define GSR_RESTART_REPLAY_DISABLE 0 +#define GSR_RESTART_REPLAY_ENABLE 1 + +/* + |seconds| can be GSR_SAVE_REPLAY_SECONDS_FULL to save the whole replay buffer. + |restart_replay| overrides the -restart-replay-on-save option for this save when it's not GSR_RESTART_REPLAY_USE_OPTION. +*/ +void gsr_recorder_save_replay(gsr_recorder *self, int seconds, int restart_replay); #endif /* GSR_RECORDER_RECORDER_H */ diff --git a/src/cli/ipc.c b/src/cli/ipc.c index ff4b852..2fb3000 100644 --- a/src/cli/ipc.c +++ b/src/cli/ipc.c @@ -37,6 +37,7 @@ typedef struct { char name[GSR_IPC_MAX_REQUEST_NAME_SIZE]; sj_Value data; bool has_data; + const char *request_end; } gsr_ipc_request; typedef struct { @@ -269,6 +270,7 @@ static bool ipc_client_send_reply(gsr_ipc *self, gsr_ipc_client *client, int64_t 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); @@ -326,18 +328,57 @@ static bool ipc_request_parse(char *data, size_t size, gsr_ipc_request *request, return true; } -static bool ipc_request_get_save_replay_seconds(const gsr_ipc_request *request, int *seconds, char *error_message, size_t error_message_size) { +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; - int64_t data_seconds = 0; - if(!gsr_json_number_to_int64(&request->data, &data_seconds) || data_seconds <= 0 || data_seconds > INT_MAX) { - snprintf(error_message, error_message_size, "expected 'data' to be the number of seconds to save, which has to be larger than 0"); + 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; } - *seconds = data_seconds; return true; } @@ -437,10 +478,12 @@ static bool ipc_handle_request(gsr_ipc *self, const gsr_ipc_request *request, ch if(strcmp(request->name, "save-replay") == 0) { int seconds = GSR_SAVE_REPLAY_SECONDS_FULL; - if(!ipc_request_get_save_replay_seconds(request, &seconds, error_message, error_message_size)) + 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, error_message, error_message_size, self->handlers.userdata); + 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); diff --git a/src/cli/main.c b/src/cli/main.c index de014a7..ac7de24 100644 --- a/src/cli/main.c +++ b/src/cli/main.c @@ -59,7 +59,7 @@ static void toggle_replay_recording_handler(int signal_value) { static void save_replay_seconds_handler(gsr_recorder *rec, int seconds) { if(rec) - gsr_recorder_save_replay(rec, seconds); + gsr_recorder_save_replay(rec, seconds, GSR_RESTART_REPLAY_USE_OPTION); else pending_save_replay_seconds = seconds; } @@ -78,7 +78,7 @@ static void apply_pending_signals(gsr_recorder *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_recorder_save_replay(rec, seconds, GSR_RESTART_REPLAY_USE_OPTION); } } @@ -186,14 +186,18 @@ static bool ipc_stop_replay_recording_handler(char *error_message, size_t error_ return true; } -static bool ipc_save_replay_handler(int seconds, char *error_message, size_t error_message_size, void *userdata) { +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; } - gsr_recorder_save_replay(recorder, seconds); + 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; } diff --git a/src/recorder/recorder.c b/src/recorder/recorder.c index fc0ccf0..3f1331f 100644 --- a/src/recorder/recorder.c +++ b/src/recorder/recorder.c @@ -101,6 +101,7 @@ struct gsr_recorder { atomic_int replay_recording_request; atomic_int replay_recording_state; atomic_int save_replay_seconds; + atomic_int save_replay_restart_replay; bool should_stop_error; bool force_iframe_frame; int audio_max_frame_size; @@ -418,6 +419,7 @@ gsr_recorder* gsr_recorder_create(const gsr_recorder_params *params, const gsr_r atomic_init(&self->replay_recording_request, GSR_REPLAY_RECORDING_REQUEST_NONE); atomic_init(&self->replay_recording_state, 0); atomic_init(&self->save_replay_seconds, 0); + atomic_init(&self->save_replay_restart_replay, GSR_RESTART_REPLAY_USE_OPTION); self->audio_max_frame_size = 1024; int error_code = GSR_ERROR_GENERIC; self->hdr = video_codec_is_hdr(params->settings->video_codec); @@ -748,11 +750,13 @@ static void recorder_poll_replay_save(gsr_recorder *self) { current_save_replay_seconds += self->settings.keyint; atomic_store(&self->save_replay_seconds, 0); + const int restart_replay_request = atomic_exchange(&self->save_replay_restart_replay, GSR_RESTART_REPLAY_USE_OPTION); + const bool restart_replay = restart_replay_request == GSR_RESTART_REPLAY_USE_OPTION ? self->settings.restart_replay_on_save : restart_replay_request == GSR_RESTART_REPLAY_ENABLE; const bool replay_start_result = gsr_replay_save_start(&self->replay_save, self->video_codec_context, GSR_VIDEO_STREAM_INDEX, &self->audio_capture, &self->encoder, &self->settings, self->file_extension, self->hdr, self->video_sources, current_save_replay_seconds); if(!replay_start_result && self->callbacks.replay_saved) self->callbacks.replay_saved(NULL, self->callbacks.userdata); - if(self->settings.restart_replay_on_save && current_save_replay_seconds == GSR_SAVE_REPLAY_SECONDS_FULL) { + if(restart_replay && current_save_replay_seconds == GSR_SAVE_REPLAY_SECONDS_FULL) { pthread_mutex_lock(&self->encoder.replay_mutex); gsr_replay_buffer_clear(self->encoder.replay_buffer); pthread_mutex_unlock(&self->encoder.replay_mutex); @@ -959,6 +963,7 @@ bool gsr_recorder_is_replay_recording(const gsr_recorder *self) { return atomic_load(&self->replay_recording_state) == 1; } -void gsr_recorder_save_replay(gsr_recorder *self, int seconds) { +void gsr_recorder_save_replay(gsr_recorder *self, int seconds, int restart_replay) { + atomic_store(&self->save_replay_restart_replay, restart_replay); atomic_store(&self->save_replay_seconds, seconds); } diff --git a/tools/gsr-cli/main.c b/tools/gsr-cli/main.c index e430964..fbfc2fa 100644 --- a/tools/gsr-cli/main.c +++ b/tools/gsr-cli/main.c @@ -19,7 +19,7 @@ #define GSR_CLI_NO_REPLY_TIMEOUT 0 static void usage(void) { - printf("usage: gsr-cli -ipc [command_argument]\n"); + printf("usage: gsr-cli -ipc [command_arguments...]\n"); printf("\n"); printf("Sends a command to a GPU Screen Recorder instance that was started with the -ipc option.\n"); printf("\n"); @@ -46,14 +46,16 @@ static void usage(void) { printf(" stop-replay-recording\n"); printf(" Stop the regular recording that runs during replay/streaming. Waits until the recording\n"); printf(" has been saved and prints the path of the saved file.\n"); - printf(" save-replay [seconds]\n"); + printf(" save-replay [seconds] [restart-replay=true|false]\n"); printf(" Save the replay. The number of seconds has to be larger than 0. The whole replay buffer is\n"); printf(" saved when no number of seconds is given. Waits until the replay has been saved and prints\n"); - printf(" the path of the saved file.\n"); + printf(" the path of the saved file. restart-replay overrides the -restart-replay-on-save option\n"); + printf(" of GPU Screen Recorder for this save.\n"); printf("\n"); printf("EXAMPLES:\n"); printf(" gsr-cli -ipc \"$XDG_RUNTIME_DIR/gsr.sock\" status\n"); printf(" gsr-cli -ipc \"$XDG_RUNTIME_DIR/gsr.sock\" save-replay 30\n"); + printf(" gsr-cli -ipc \"$XDG_RUNTIME_DIR/gsr.sock\" save-replay restart-replay=true\n"); fflush(stdout); } @@ -272,7 +274,7 @@ int main(int argc, char **argv) { return 1; } - if(argc > 5) { + if(argc > 6) { gsr_log(GSR_LOG_LEVEL_ERROR, "too many arguments"); usage(); return 1; @@ -280,23 +282,53 @@ int main(int argc, char **argv) { const char *socket_filepath = argv[2]; const char *command = argv[3]; - const char *command_argument = argc == 5 ? argv[4] : NULL; + const char *command_argument = argc >= 5 ? argv[4] : NULL; + const char *command_argument2 = argc >= 6 ? argv[5] : NULL; char request[GSR_CLI_MAX_REQUEST_SIZE]; if(strcmp(command, "save-replay") == 0) { - if(command_argument) { - int64_t seconds = 0; - if(!string_to_int64(command_argument, &seconds) || seconds <= 0 || seconds > INT_MAX) { - gsr_log(GSR_LOG_LEVEL_ERROR, "expected the number of seconds to save to be an integer larger than 0, got: '%s'", command_argument); + bool has_seconds = false; + int64_t seconds = 0; + const char *restart_replay = NULL; + + const char *command_arguments[2] = { command_argument, command_argument2 }; + for(int i = 0; i < 2; ++i) { + const char *argument = command_arguments[i]; + if(!argument) + continue; + + if(strncmp(argument, "restart-replay=", 15) == 0) { + restart_replay = argument + 15; + if(strcmp(restart_replay, "true") != 0 && strcmp(restart_replay, "false") != 0) { + gsr_log(GSR_LOG_LEVEL_ERROR, "expected restart-replay to be either true or false, got: '%s'", restart_replay); + return 1; + } + } else if(!has_seconds && string_to_int64(argument, &seconds) && seconds > 0 && seconds <= INT_MAX) { + has_seconds = true; + } else { + gsr_log(GSR_LOG_LEVEL_ERROR, "expected the argument to be the number of seconds to save (an integer larger than 0) or restart-replay=true|false, got: '%s'", argument); return 1; } - snprintf(request, sizeof(request), "{\"id\":%d,\"name\":\"save-replay\",\"data\":%" PRIi64 "}\n", GSR_CLI_REQUEST_ID, seconds); - } else { - snprintf(request, sizeof(request), "{\"id\":%d,\"name\":\"save-replay\"}\n", GSR_CLI_REQUEST_ID); } + + if(restart_replay && has_seconds) + snprintf(request, sizeof(request), "{\"id\":%d,\"name\":\"save-replay\",\"data\":{\"seconds\":%" PRIi64 ",\"restart-replay\":%s}}\n", GSR_CLI_REQUEST_ID, seconds, restart_replay); + else if(restart_replay) + snprintf(request, sizeof(request), "{\"id\":%d,\"name\":\"save-replay\",\"data\":{\"restart-replay\":%s}}\n", GSR_CLI_REQUEST_ID, restart_replay); + else if(has_seconds) + snprintf(request, sizeof(request), "{\"id\":%d,\"name\":\"save-replay\",\"data\":{\"seconds\":%" PRIi64 "}}\n", GSR_CLI_REQUEST_ID, seconds); + else + snprintf(request, sizeof(request), "{\"id\":%d,\"name\":\"save-replay\"}\n", GSR_CLI_REQUEST_ID); + return send_request(socket_filepath, request, GSR_CLI_NO_REPLY_TIMEOUT); } + if(command_argument2) { + gsr_log(GSR_LOG_LEVEL_ERROR, "the '%s' command doesn't take more than one argument", command); + usage(); + return 1; + } + if(strcmp(command, "set-paused") == 0) { if(!command_argument || (strcmp(command_argument, "true") != 0 && strcmp(command_argument, "false") != 0)) { gsr_log(GSR_LOG_LEVEL_ERROR, "the 'set-paused' command expects either true or false as the argument"); -- cgit v1.2.3 From 5db30f3fb99545524fc80f87ecc488972aec1711 Mon Sep 17 00:00:00 2001 From: dec05eba Date: Thu, 6 Aug 2026 12:56:48 +0200 Subject: Clear replay data in replay save thread to not block the main capture thread --- include/recorder/replay_save.h | 10 ++++++++-- include/replay_buffer/replay_buffer.h | 9 +++++++++ src/recorder/recorder.c | 8 +------- src/recorder/replay_save.c | 17 ++++++++++++++++- src/replay_buffer/replay_buffer.c | 4 ++++ src/replay_buffer/replay_buffer_disk.c | 31 +++++++++++++++++++++++++++++++ src/replay_buffer/replay_buffer_ram.c | 27 +++++++++++++++++++++++++++ 7 files changed, 96 insertions(+), 10 deletions(-) diff --git a/include/recorder/replay_save.h b/include/recorder/replay_save.h index 7f48851..ac4eb98 100644 --- a/include/recorder/replay_save.h +++ b/include/recorder/replay_save.h @@ -37,14 +37,20 @@ typedef struct { gsr_audio_pts_offset *audio_pts_offsets; size_t num_audio_pts_offsets; gsr_replay_buffer *cloned_replay_buffer; + /* The data that the replay buffer had before it was restarted, free'd on the replay save thread */ + gsr_replay_buffer *restarted_replay_buffer_data; gsr_encoder *encoder; } gsr_replay_save; void gsr_replay_save_init(gsr_replay_save *self); bool gsr_replay_save_is_running(const gsr_replay_save *self); -/* Returns false if the replay failed to start. |current_save_replay_seconds| can be GSR_SAVE_REPLAY_SECONDS_FULL to save the whole replay buffer */ -bool gsr_replay_save_start(gsr_replay_save *self, AVCodecContext *video_codec_context, int video_stream_index, const gsr_audio_capture *audio_capture, gsr_encoder *encoder, const gsr_recorder_settings *settings, const char *file_extension, bool hdr, gsr_video_sources *video_sources, int current_save_replay_seconds); +/* + Returns false if the replay failed to start. |current_save_replay_seconds| can be GSR_SAVE_REPLAY_SECONDS_FULL to save the whole replay buffer. + The replay buffer of |encoder| is restarted (emptied) when |restart_replay| is set, which is done without blocking the + thread that appends to the replay buffer by free'ing the old data on the replay save thread. +*/ +bool gsr_replay_save_start(gsr_replay_save *self, AVCodecContext *video_codec_context, int video_stream_index, const gsr_audio_capture *audio_capture, gsr_encoder *encoder, const gsr_recorder_settings *settings, const char *file_extension, bool hdr, gsr_video_sources *video_sources, int current_save_replay_seconds, bool restart_replay); /* Returns true when the replay finished saving, in which case |success| and |output_filepath| are set. |output_filepath| is empty when nothing was saved */ bool gsr_replay_save_poll(gsr_replay_save *self, bool *success, const char **output_filepath); /* Waits for an ongoing replay save to finish. Returns the same values as gsr_replay_save_poll */ diff --git a/include/replay_buffer/replay_buffer.h b/include/replay_buffer/replay_buffer.h index 6e91103..c036b1c 100644 --- a/include/replay_buffer/replay_buffer.h +++ b/include/replay_buffer/replay_buffer.h @@ -21,6 +21,8 @@ struct gsr_replay_buffer { uint8_t* (*iterator_get_packet_data)(gsr_replay_buffer *self, gsr_replay_buffer_iterator iterator); /* The clone has to be destroyed before the replay buffer it clones is destroyed */ gsr_replay_buffer* (*clone)(gsr_replay_buffer *self); + /* Returns NULL on failure, in which case |self| is left untouched */ + gsr_replay_buffer* (*take_data)(gsr_replay_buffer *self); /* Returns {0, 0} if replay buffer is empty */ gsr_replay_buffer_iterator (*find_packet_index_by_time_passed)(gsr_replay_buffer *self, int seconds); /* Returns {-1, 0} if not found */ @@ -38,6 +40,13 @@ AVPacket* gsr_replay_buffer_iterator_get_packet(gsr_replay_buffer *self, gsr_rep uint8_t* gsr_replay_buffer_iterator_get_packet_data(gsr_replay_buffer *self, gsr_replay_buffer_iterator iterator); /* The clone has to be destroyed before the replay buffer it clones is destroyed */ gsr_replay_buffer* gsr_replay_buffer_clone(gsr_replay_buffer *self); +/* + Moves the data of |self| into a new replay buffer and leaves |self| empty and ready to be used again. + This is a cheap operation, unlike gsr_replay_buffer_clear, because freeing the data is deferred to + gsr_replay_buffer_destroy of the returned replay buffer, which can be done without blocking |self|. + Returns NULL on failure, in which case |self| is left untouched. +*/ +gsr_replay_buffer* gsr_replay_buffer_take_data(gsr_replay_buffer *self); /* Returns {0, 0} if replay buffer is empty */ gsr_replay_buffer_iterator gsr_replay_buffer_find_packet_index_by_time_passed(gsr_replay_buffer *self, int seconds); /* Returns {-1, 0} if not found */ diff --git a/src/recorder/recorder.c b/src/recorder/recorder.c index 3f1331f..949ed99 100644 --- a/src/recorder/recorder.c +++ b/src/recorder/recorder.c @@ -752,15 +752,9 @@ static void recorder_poll_replay_save(gsr_recorder *self) { atomic_store(&self->save_replay_seconds, 0); const int restart_replay_request = atomic_exchange(&self->save_replay_restart_replay, GSR_RESTART_REPLAY_USE_OPTION); const bool restart_replay = restart_replay_request == GSR_RESTART_REPLAY_USE_OPTION ? self->settings.restart_replay_on_save : restart_replay_request == GSR_RESTART_REPLAY_ENABLE; - const bool replay_start_result = gsr_replay_save_start(&self->replay_save, self->video_codec_context, GSR_VIDEO_STREAM_INDEX, &self->audio_capture, &self->encoder, &self->settings, self->file_extension, self->hdr, self->video_sources, current_save_replay_seconds); + const bool replay_start_result = gsr_replay_save_start(&self->replay_save, self->video_codec_context, GSR_VIDEO_STREAM_INDEX, &self->audio_capture, &self->encoder, &self->settings, self->file_extension, self->hdr, self->video_sources, current_save_replay_seconds, restart_replay && current_save_replay_seconds == GSR_SAVE_REPLAY_SECONDS_FULL); if(!replay_start_result && self->callbacks.replay_saved) self->callbacks.replay_saved(NULL, self->callbacks.userdata); - - if(restart_replay && current_save_replay_seconds == GSR_SAVE_REPLAY_SECONDS_FULL) { - pthread_mutex_lock(&self->encoder.replay_mutex); - gsr_replay_buffer_clear(self->encoder.replay_buffer); - pthread_mutex_unlock(&self->encoder.replay_mutex); - } } } diff --git a/src/recorder/replay_save.c b/src/recorder/replay_save.c index 3251616..55cf35d 100644 --- a/src/recorder/replay_save.c +++ b/src/recorder/replay_save.c @@ -23,6 +23,15 @@ static void gsr_replay_save_cleanup(gsr_replay_save *self) { self->cloned_replay_buffer = NULL; } + /* + This is the data that the replay buffer had before the replay was saved and nothing references it anymore, + so it can be free'd without holding the replay mutex. This can take a while when the replay buffer is large. + */ + if(self->restarted_replay_buffer_data) { + gsr_replay_buffer_destroy(self->restarted_replay_buffer_data); + self->restarted_replay_buffer_data = NULL; + } + if(self->audio_pts_offsets) { free(self->audio_pts_offsets); self->audio_pts_offsets = NULL; @@ -116,7 +125,7 @@ static void* replay_save_thread(void *userdata) { return NULL; } -bool gsr_replay_save_start(gsr_replay_save *self, AVCodecContext *video_codec_context, int video_stream_index, const gsr_audio_capture *audio_capture, gsr_encoder *encoder, const gsr_recorder_settings *settings, const char *file_extension, bool hdr, gsr_video_sources *video_sources, int current_save_replay_seconds) { +bool gsr_replay_save_start(gsr_replay_save *self, AVCodecContext *video_codec_context, int video_stream_index, const gsr_audio_capture *audio_capture, gsr_encoder *encoder, const gsr_recorder_settings *settings, const char *file_extension, bool hdr, gsr_video_sources *video_sources, int current_save_replay_seconds, bool restart_replay) { if(self->thread_created) return true; @@ -125,10 +134,16 @@ bool gsr_replay_save_start(gsr_replay_save *self, AVCodecContext *video_codec_co self->video_stream_index = video_stream_index; self->output_filepath[0] = '\0'; self->success = false; + self->restarted_replay_buffer_data = NULL; atomic_store(&self->finished, 0); pthread_mutex_lock(&encoder->replay_mutex); self->cloned_replay_buffer = gsr_replay_buffer_clone(encoder->replay_buffer); + if(self->cloned_replay_buffer && restart_replay) { + self->restarted_replay_buffer_data = gsr_replay_buffer_take_data(encoder->replay_buffer); + if(!self->restarted_replay_buffer_data) + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_replay_save: failed to restart the replay buffer"); + } pthread_mutex_unlock(&encoder->replay_mutex); if(!self->cloned_replay_buffer) { /* TODO: Return this error to mark the replay as failed */ diff --git a/src/replay_buffer/replay_buffer.c b/src/replay_buffer/replay_buffer.c index 08ed1f1..458b359 100644 --- a/src/replay_buffer/replay_buffer.c +++ b/src/replay_buffer/replay_buffer.c @@ -42,6 +42,10 @@ gsr_replay_buffer* gsr_replay_buffer_clone(gsr_replay_buffer *self) { return self->clone(self); } +gsr_replay_buffer* gsr_replay_buffer_take_data(gsr_replay_buffer *self) { + return self->take_data(self); +} + gsr_replay_buffer_iterator gsr_replay_buffer_find_packet_index_by_time_passed(gsr_replay_buffer *self, int seconds) { return self->find_packet_index_by_time_passed(self, seconds); } diff --git a/src/replay_buffer/replay_buffer_disk.c b/src/replay_buffer/replay_buffer_disk.c index ffc59cc..56244f9 100644 --- a/src/replay_buffer/replay_buffer_disk.c +++ b/src/replay_buffer/replay_buffer_disk.c @@ -254,6 +254,36 @@ static uint8_t* gsr_replay_buffer_disk_iterator_get_packet_data(gsr_replay_buffe return packet_data; } +static gsr_replay_buffer* gsr_replay_buffer_disk_take_data(gsr_replay_buffer *replay_buffer) { + gsr_replay_buffer_disk *self = (gsr_replay_buffer_disk*)replay_buffer; + gsr_replay_buffer_disk *destination = calloc(1, sizeof(gsr_replay_buffer_disk)); + if(!destination) + return NULL; + + gsr_replay_buffer_disk_set_impl_funcs(destination); + + destination->replay_buffer_time = self->replay_buffer_time; + destination->storage_counter = self->storage_counter; + destination->storage_num_bytes_written = self->storage_num_bytes_written; + destination->storage_fd = self->storage_fd; + + for(size_t i = 0; i < self->num_files; ++i) { + destination->files[i] = self->files[i]; + } + destination->num_files = self->num_files; + + snprintf(destination->replay_directory, sizeof(destination->replay_directory), "%s", self->replay_directory); + /* |self| keeps writing to the same directory, so it's the one that removes it */ + destination->owns_directory = false; + + self->num_files = 0; + self->storage_fd = 0; + self->storage_num_bytes_written = 0; + /* |storage_counter| is not reset, to not overwrite the files that were moved to |destination| */ + + return (gsr_replay_buffer*)destination; +} + static gsr_replay_buffer* gsr_replay_buffer_disk_clone(gsr_replay_buffer *replay_buffer) { gsr_replay_buffer_disk *self = (gsr_replay_buffer_disk*)replay_buffer; gsr_replay_buffer_disk *destination = calloc(1, sizeof(gsr_replay_buffer_disk)); @@ -398,6 +428,7 @@ static void gsr_replay_buffer_disk_set_impl_funcs(gsr_replay_buffer_disk *self) self->replay_buffer.iterator_get_packet = gsr_replay_buffer_disk_iterator_get_packet; self->replay_buffer.iterator_get_packet_data = gsr_replay_buffer_disk_iterator_get_packet_data; self->replay_buffer.clone = gsr_replay_buffer_disk_clone; + self->replay_buffer.take_data = gsr_replay_buffer_disk_take_data; self->replay_buffer.find_packet_index_by_time_passed = gsr_replay_buffer_disk_find_file_index_by_time_passed; self->replay_buffer.find_keyframe = gsr_replay_buffer_disk_find_keyframe; self->replay_buffer.iterator_next = gsr_replay_buffer_disk_iterator_next; diff --git a/src/replay_buffer/replay_buffer_ram.c b/src/replay_buffer/replay_buffer_ram.c index 5508a12..680517d 100644 --- a/src/replay_buffer/replay_buffer_ram.c +++ b/src/replay_buffer/replay_buffer_ram.c @@ -125,6 +125,32 @@ static uint8_t* gsr_replay_buffer_ram_iterator_get_packet_data(gsr_replay_buffer return NULL; } +static gsr_replay_buffer* gsr_replay_buffer_ram_take_data(gsr_replay_buffer *replay_buffer) { + gsr_replay_buffer_ram *self = (gsr_replay_buffer_ram*)replay_buffer; + gsr_av_packet_ram **packets = calloc(self->capacity_num_packets, sizeof(gsr_av_packet_ram*)); + if(!packets) + return NULL; + + gsr_replay_buffer_ram *destination = calloc(1, sizeof(gsr_replay_buffer_ram)); + if(!destination) { + free(packets); + return NULL; + } + + gsr_replay_buffer_ram_set_impl_funcs(destination); + + destination->packets = self->packets; + destination->capacity_num_packets = self->capacity_num_packets; + destination->num_packets = self->num_packets; + destination->index = self->index; + + self->packets = packets; + self->num_packets = 0; + self->index = 0; + + return (gsr_replay_buffer*)destination; +} + static gsr_replay_buffer* gsr_replay_buffer_ram_clone(gsr_replay_buffer *replay_buffer) { gsr_replay_buffer_ram *self = (gsr_replay_buffer_ram*)replay_buffer; gsr_replay_buffer_ram *destination = calloc(1, sizeof(gsr_replay_buffer_ram)); @@ -210,6 +236,7 @@ static void gsr_replay_buffer_ram_set_impl_funcs(gsr_replay_buffer_ram *self) { self->replay_buffer.iterator_get_packet = gsr_replay_buffer_ram_iterator_get_packet; self->replay_buffer.iterator_get_packet_data = gsr_replay_buffer_ram_iterator_get_packet_data; self->replay_buffer.clone = gsr_replay_buffer_ram_clone; + self->replay_buffer.take_data = gsr_replay_buffer_ram_take_data; self->replay_buffer.find_packet_index_by_time_passed = gsr_replay_buffer_ram_find_packet_index_by_time_passed; self->replay_buffer.find_keyframe = gsr_replay_buffer_ram_find_keyframe; self->replay_buffer.iterator_next = gsr_replay_buffer_ram_iterator_next; -- cgit v1.2.3 From e31968fc4074df739c00947b993ee19be4ef1caa Mon Sep 17 00:00:00 2001 From: dec05eba Date: Thu, 6 Aug 2026 13:02:59 +0200 Subject: Faster shutdown time when several gb of replay data is in ram --- include/encoder/encoder.h | 6 +++++- include/recorder/recorder.h | 3 ++- include/replay_buffer/replay_buffer.h | 8 ++++++++ src/cli/main.c | 2 +- src/encoder/encoder.c | 13 ++++++++----- src/recorder/recorder.c | 6 +++--- src/replay_buffer/replay_buffer.c | 5 +++++ src/replay_buffer/replay_buffer_disk.c | 2 ++ src/replay_buffer/replay_buffer_ram.c | 6 ++++++ 9 files changed, 40 insertions(+), 11 deletions(-) diff --git a/include/encoder/encoder.h b/include/encoder/encoder.h index b46deb5..15009f1 100644 --- a/include/encoder/encoder.h +++ b/include/encoder/encoder.h @@ -41,7 +41,11 @@ typedef struct { } gsr_encoder; bool gsr_encoder_init(gsr_encoder *self, gsr_replay_storage replay_storage, size_t replay_buffer_num_packets, double replay_buffer_time, const char *replay_directory); -void gsr_encoder_deinit(gsr_encoder *self); +/* + When |exiting| is set the memory of the replay buffer is left to the operating system to free, which makes + this much faster when the replay buffer holds several gigabytes of data. See gsr_replay_buffer_destroy_at_exit. +*/ +void gsr_encoder_deinit(gsr_encoder *self, bool exiting); void gsr_encoder_receive_packets(gsr_encoder *self, AVCodecContext *codec_context, int64_t pts, int stream_index); /* Returns the id to the recording destination, or -1 on error */ diff --git a/include/recorder/recorder.h b/include/recorder/recorder.h index 5721a98..9ff13bd 100644 --- a/include/recorder/recorder.h +++ b/include/recorder/recorder.h @@ -39,7 +39,8 @@ typedef struct { /* Returns NULL on failure and sets |error| to a |gsr_error| value */ gsr_recorder* gsr_recorder_create(const gsr_recorder_params *params, const gsr_recorder_callbacks *callbacks, int *error); -void gsr_recorder_destroy(gsr_recorder *self); +/* This is only called when the program is exiting, so memory that the operating system frees on exit isn't free'd. Only set exiting to true if the program is exiting */ +void gsr_recorder_destroy(gsr_recorder *self, bool exiting); /* Returns a |gsr_error| value. Records until gsr_recorder_stop is called or until the capture target is gone */ int gsr_recorder_run(gsr_recorder *self); diff --git a/include/replay_buffer/replay_buffer.h b/include/replay_buffer/replay_buffer.h index c036b1c..3ac452a 100644 --- a/include/replay_buffer/replay_buffer.h +++ b/include/replay_buffer/replay_buffer.h @@ -14,6 +14,7 @@ typedef struct { struct gsr_replay_buffer { void (*destroy)(gsr_replay_buffer *self); + void (*destroy_at_exit)(gsr_replay_buffer *self); bool (*append)(gsr_replay_buffer *self, const AVPacket *av_packet, double timestamp); void (*clear)(gsr_replay_buffer *self); AVPacket* (*iterator_get_packet)(gsr_replay_buffer *self, gsr_replay_buffer_iterator iterator); @@ -32,6 +33,13 @@ struct gsr_replay_buffer { gsr_replay_buffer* gsr_replay_buffer_create(gsr_replay_storage replay_storage, const char *replay_directory, double replay_buffer_time, size_t replay_buffer_num_packets); void gsr_replay_buffer_destroy(gsr_replay_buffer *self); +/* + Only frees the resources that the operating system doesn't free when the process exits, such as files, + and leaves the memory to the operating system to free. Freeing a replay buffer that holds several gigabytes + of data takes seconds, which is time wasted when the process is exiting anyway. + |self| can't be used after this. +*/ +void gsr_replay_buffer_destroy_at_exit(gsr_replay_buffer *self); bool gsr_replay_buffer_append(gsr_replay_buffer *self, const AVPacket *av_packet, double timestamp); void gsr_replay_buffer_clear(gsr_replay_buffer *self); diff --git a/src/cli/main.c b/src/cli/main.c index ac7de24..aaa315c 100644 --- a/src/cli/main.c +++ b/src/cli/main.c @@ -500,7 +500,7 @@ static int record(args_parser *arg_parser, gsr_windowing *windowing, gsr_capture 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); + gsr_recorder_destroy(recorder, true); recorder = NULL; return run_result; } diff --git a/src/encoder/encoder.c b/src/encoder/encoder.c index 3aebb7a..e9b71d7 100644 --- a/src/encoder/encoder.c +++ b/src/encoder/encoder.c @@ -44,14 +44,14 @@ bool gsr_encoder_init(gsr_encoder *self, gsr_replay_storage replay_storage, size if(pthread_mutex_init(&self->file_write_mutex, NULL) != 0) { gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_encoder_init: failed to create mutex"); - gsr_encoder_deinit(self); + gsr_encoder_deinit(self, false); return false; } self->file_write_mutex_created = true; if(pthread_mutex_init(&self->replay_mutex, NULL) != 0) { gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_encoder_init: failed to create mutex"); - gsr_encoder_deinit(self); + gsr_encoder_deinit(self, false); return false; } self->replay_mutex_created = true; @@ -60,7 +60,7 @@ bool gsr_encoder_init(gsr_encoder *self, gsr_replay_storage replay_storage, size self->replay_buffer = gsr_replay_buffer_create(replay_storage, replay_directory, replay_buffer_time, replay_buffer_num_packets); if(!self->replay_buffer) { gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_encoder_init: failed to create replay buffer"); - gsr_encoder_deinit(self); + gsr_encoder_deinit(self, false); return false; } } @@ -68,7 +68,7 @@ bool gsr_encoder_init(gsr_encoder *self, gsr_replay_storage replay_storage, size return true; } -void gsr_encoder_deinit(gsr_encoder *self) { +void gsr_encoder_deinit(gsr_encoder *self, bool exiting) { if(self->file_write_mutex_created) pthread_mutex_lock(&self->file_write_mutex); for(size_t i = 0; i < self->num_recording_destinations; ++i) { @@ -81,7 +81,10 @@ void gsr_encoder_deinit(gsr_encoder *self) { if(self->replay_buffer) { pthread_mutex_lock(&self->replay_mutex); - gsr_replay_buffer_destroy(self->replay_buffer); + if(exiting) + gsr_replay_buffer_destroy_at_exit(self->replay_buffer); + else + gsr_replay_buffer_destroy(self->replay_buffer); self->replay_buffer = NULL; pthread_mutex_unlock(&self->replay_mutex); } diff --git a/src/recorder/recorder.c b/src/recorder/recorder.c index 949ed99..f24495c 100644 --- a/src/recorder/recorder.c +++ b/src/recorder/recorder.c @@ -450,7 +450,7 @@ gsr_recorder* gsr_recorder_create(const gsr_recorder_params *params, const gsr_r fail: *error = error_code; - gsr_recorder_destroy(self); + gsr_recorder_destroy(self, false); return NULL; } @@ -892,7 +892,7 @@ static void gsr_recorder_stop_recording(gsr_recorder *self) { self->callbacks.recording_stopped(self->settings.filename, self->callbacks.userdata); } -void gsr_recorder_destroy(gsr_recorder *self) { +void gsr_recorder_destroy(gsr_recorder *self, bool exiting) { if(!self) return; @@ -909,7 +909,7 @@ void gsr_recorder_destroy(gsr_recorder *self) { gsr_video_encoder_destroy(self->video_encoder, self->video_codec_context); if(self->encoder_initialized) - gsr_encoder_deinit(&self->encoder); + gsr_encoder_deinit(&self->encoder, exiting); gsr_video_sources_deinit(&self->video_sources_data); diff --git a/src/replay_buffer/replay_buffer.c b/src/replay_buffer/replay_buffer.c index 458b359..4958bba 100644 --- a/src/replay_buffer/replay_buffer.c +++ b/src/replay_buffer/replay_buffer.c @@ -22,6 +22,11 @@ void gsr_replay_buffer_destroy(gsr_replay_buffer *self) { free(self); } +void gsr_replay_buffer_destroy_at_exit(gsr_replay_buffer *self) { + self->destroy_at_exit(self); + /* |self| is intentionally not free'd, the operating system does that when the process exits */ +} + bool gsr_replay_buffer_append(gsr_replay_buffer *self, const AVPacket *av_packet, double timestamp) { return self->append(self, av_packet, timestamp); } diff --git a/src/replay_buffer/replay_buffer_disk.c b/src/replay_buffer/replay_buffer_disk.c index 56244f9..da82df5 100644 --- a/src/replay_buffer/replay_buffer_disk.c +++ b/src/replay_buffer/replay_buffer_disk.c @@ -423,6 +423,8 @@ static void get_current_time(char *time_str, size_t time_str_size) { static void gsr_replay_buffer_disk_set_impl_funcs(gsr_replay_buffer_disk *self) { self->replay_buffer.destroy = gsr_replay_buffer_disk_destroy; + /* The files and the directory have to be removed even when the process exits */ + self->replay_buffer.destroy_at_exit = gsr_replay_buffer_disk_destroy; self->replay_buffer.append = gsr_replay_buffer_disk_append; self->replay_buffer.clear = gsr_replay_buffer_disk_clear; self->replay_buffer.iterator_get_packet = gsr_replay_buffer_disk_iterator_get_packet; diff --git a/src/replay_buffer/replay_buffer_ram.c b/src/replay_buffer/replay_buffer_ram.c index 680517d..ce3914b 100644 --- a/src/replay_buffer/replay_buffer_ram.c +++ b/src/replay_buffer/replay_buffer_ram.c @@ -70,6 +70,11 @@ static void gsr_replay_buffer_ram_destroy(gsr_replay_buffer *replay_buffer) { self->index = 0; } +/* The replay buffer only holds memory, which the operating system frees when the process exits */ +static void gsr_replay_buffer_ram_destroy_at_exit(gsr_replay_buffer *replay_buffer) { + (void)replay_buffer; +} + static bool gsr_replay_buffer_ram_append(gsr_replay_buffer *replay_buffer, const AVPacket *av_packet, double timestamp) { gsr_replay_buffer_ram *self = (gsr_replay_buffer_ram*)replay_buffer; gsr_av_packet_ram *packet = gsr_av_packet_ram_create(av_packet, timestamp); @@ -231,6 +236,7 @@ static bool gsr_replay_buffer_ram_iterator_next(gsr_replay_buffer *replay_buffer static void gsr_replay_buffer_ram_set_impl_funcs(gsr_replay_buffer_ram *self) { self->replay_buffer.destroy = gsr_replay_buffer_ram_destroy; + self->replay_buffer.destroy_at_exit = gsr_replay_buffer_ram_destroy_at_exit; self->replay_buffer.append = gsr_replay_buffer_ram_append; self->replay_buffer.clear = gsr_replay_buffer_ram_clear; self->replay_buffer.iterator_get_packet = gsr_replay_buffer_ram_iterator_get_packet; -- cgit v1.2.3 From 90ac49f372f4c3d3d12f4d0d58ab0473a86821d5 Mon Sep 17 00:00:00 2001 From: dec05eba Date: Fri, 7 Aug 2026 13:53:40 +0200 Subject: Revert "Clear replay data in replay save thread to not block the main capture thread" This reverts commit 5db30f3fb99545524fc80f87ecc488972aec1711. --- include/recorder/replay_save.h | 10 ++-------- include/replay_buffer/replay_buffer.h | 9 --------- src/recorder/recorder.c | 8 +++++++- src/recorder/replay_save.c | 17 +---------------- src/replay_buffer/replay_buffer.c | 4 ---- src/replay_buffer/replay_buffer_disk.c | 31 ------------------------------- src/replay_buffer/replay_buffer_ram.c | 27 --------------------------- 7 files changed, 10 insertions(+), 96 deletions(-) diff --git a/include/recorder/replay_save.h b/include/recorder/replay_save.h index ac4eb98..7f48851 100644 --- a/include/recorder/replay_save.h +++ b/include/recorder/replay_save.h @@ -37,20 +37,14 @@ typedef struct { gsr_audio_pts_offset *audio_pts_offsets; size_t num_audio_pts_offsets; gsr_replay_buffer *cloned_replay_buffer; - /* The data that the replay buffer had before it was restarted, free'd on the replay save thread */ - gsr_replay_buffer *restarted_replay_buffer_data; gsr_encoder *encoder; } gsr_replay_save; void gsr_replay_save_init(gsr_replay_save *self); bool gsr_replay_save_is_running(const gsr_replay_save *self); -/* - Returns false if the replay failed to start. |current_save_replay_seconds| can be GSR_SAVE_REPLAY_SECONDS_FULL to save the whole replay buffer. - The replay buffer of |encoder| is restarted (emptied) when |restart_replay| is set, which is done without blocking the - thread that appends to the replay buffer by free'ing the old data on the replay save thread. -*/ -bool gsr_replay_save_start(gsr_replay_save *self, AVCodecContext *video_codec_context, int video_stream_index, const gsr_audio_capture *audio_capture, gsr_encoder *encoder, const gsr_recorder_settings *settings, const char *file_extension, bool hdr, gsr_video_sources *video_sources, int current_save_replay_seconds, bool restart_replay); +/* Returns false if the replay failed to start. |current_save_replay_seconds| can be GSR_SAVE_REPLAY_SECONDS_FULL to save the whole replay buffer */ +bool gsr_replay_save_start(gsr_replay_save *self, AVCodecContext *video_codec_context, int video_stream_index, const gsr_audio_capture *audio_capture, gsr_encoder *encoder, const gsr_recorder_settings *settings, const char *file_extension, bool hdr, gsr_video_sources *video_sources, int current_save_replay_seconds); /* Returns true when the replay finished saving, in which case |success| and |output_filepath| are set. |output_filepath| is empty when nothing was saved */ bool gsr_replay_save_poll(gsr_replay_save *self, bool *success, const char **output_filepath); /* Waits for an ongoing replay save to finish. Returns the same values as gsr_replay_save_poll */ diff --git a/include/replay_buffer/replay_buffer.h b/include/replay_buffer/replay_buffer.h index 3ac452a..bf2e4e6 100644 --- a/include/replay_buffer/replay_buffer.h +++ b/include/replay_buffer/replay_buffer.h @@ -22,8 +22,6 @@ struct gsr_replay_buffer { uint8_t* (*iterator_get_packet_data)(gsr_replay_buffer *self, gsr_replay_buffer_iterator iterator); /* The clone has to be destroyed before the replay buffer it clones is destroyed */ gsr_replay_buffer* (*clone)(gsr_replay_buffer *self); - /* Returns NULL on failure, in which case |self| is left untouched */ - gsr_replay_buffer* (*take_data)(gsr_replay_buffer *self); /* Returns {0, 0} if replay buffer is empty */ gsr_replay_buffer_iterator (*find_packet_index_by_time_passed)(gsr_replay_buffer *self, int seconds); /* Returns {-1, 0} if not found */ @@ -48,13 +46,6 @@ AVPacket* gsr_replay_buffer_iterator_get_packet(gsr_replay_buffer *self, gsr_rep uint8_t* gsr_replay_buffer_iterator_get_packet_data(gsr_replay_buffer *self, gsr_replay_buffer_iterator iterator); /* The clone has to be destroyed before the replay buffer it clones is destroyed */ gsr_replay_buffer* gsr_replay_buffer_clone(gsr_replay_buffer *self); -/* - Moves the data of |self| into a new replay buffer and leaves |self| empty and ready to be used again. - This is a cheap operation, unlike gsr_replay_buffer_clear, because freeing the data is deferred to - gsr_replay_buffer_destroy of the returned replay buffer, which can be done without blocking |self|. - Returns NULL on failure, in which case |self| is left untouched. -*/ -gsr_replay_buffer* gsr_replay_buffer_take_data(gsr_replay_buffer *self); /* Returns {0, 0} if replay buffer is empty */ gsr_replay_buffer_iterator gsr_replay_buffer_find_packet_index_by_time_passed(gsr_replay_buffer *self, int seconds); /* Returns {-1, 0} if not found */ diff --git a/src/recorder/recorder.c b/src/recorder/recorder.c index f24495c..901077c 100644 --- a/src/recorder/recorder.c +++ b/src/recorder/recorder.c @@ -752,9 +752,15 @@ static void recorder_poll_replay_save(gsr_recorder *self) { atomic_store(&self->save_replay_seconds, 0); const int restart_replay_request = atomic_exchange(&self->save_replay_restart_replay, GSR_RESTART_REPLAY_USE_OPTION); const bool restart_replay = restart_replay_request == GSR_RESTART_REPLAY_USE_OPTION ? self->settings.restart_replay_on_save : restart_replay_request == GSR_RESTART_REPLAY_ENABLE; - const bool replay_start_result = gsr_replay_save_start(&self->replay_save, self->video_codec_context, GSR_VIDEO_STREAM_INDEX, &self->audio_capture, &self->encoder, &self->settings, self->file_extension, self->hdr, self->video_sources, current_save_replay_seconds, restart_replay && current_save_replay_seconds == GSR_SAVE_REPLAY_SECONDS_FULL); + const bool replay_start_result = gsr_replay_save_start(&self->replay_save, self->video_codec_context, GSR_VIDEO_STREAM_INDEX, &self->audio_capture, &self->encoder, &self->settings, self->file_extension, self->hdr, self->video_sources, current_save_replay_seconds); if(!replay_start_result && self->callbacks.replay_saved) self->callbacks.replay_saved(NULL, self->callbacks.userdata); + + if(restart_replay && current_save_replay_seconds == GSR_SAVE_REPLAY_SECONDS_FULL) { + pthread_mutex_lock(&self->encoder.replay_mutex); + gsr_replay_buffer_clear(self->encoder.replay_buffer); + pthread_mutex_unlock(&self->encoder.replay_mutex); + } } } diff --git a/src/recorder/replay_save.c b/src/recorder/replay_save.c index 55cf35d..3251616 100644 --- a/src/recorder/replay_save.c +++ b/src/recorder/replay_save.c @@ -23,15 +23,6 @@ static void gsr_replay_save_cleanup(gsr_replay_save *self) { self->cloned_replay_buffer = NULL; } - /* - This is the data that the replay buffer had before the replay was saved and nothing references it anymore, - so it can be free'd without holding the replay mutex. This can take a while when the replay buffer is large. - */ - if(self->restarted_replay_buffer_data) { - gsr_replay_buffer_destroy(self->restarted_replay_buffer_data); - self->restarted_replay_buffer_data = NULL; - } - if(self->audio_pts_offsets) { free(self->audio_pts_offsets); self->audio_pts_offsets = NULL; @@ -125,7 +116,7 @@ static void* replay_save_thread(void *userdata) { return NULL; } -bool gsr_replay_save_start(gsr_replay_save *self, AVCodecContext *video_codec_context, int video_stream_index, const gsr_audio_capture *audio_capture, gsr_encoder *encoder, const gsr_recorder_settings *settings, const char *file_extension, bool hdr, gsr_video_sources *video_sources, int current_save_replay_seconds, bool restart_replay) { +bool gsr_replay_save_start(gsr_replay_save *self, AVCodecContext *video_codec_context, int video_stream_index, const gsr_audio_capture *audio_capture, gsr_encoder *encoder, const gsr_recorder_settings *settings, const char *file_extension, bool hdr, gsr_video_sources *video_sources, int current_save_replay_seconds) { if(self->thread_created) return true; @@ -134,16 +125,10 @@ bool gsr_replay_save_start(gsr_replay_save *self, AVCodecContext *video_codec_co self->video_stream_index = video_stream_index; self->output_filepath[0] = '\0'; self->success = false; - self->restarted_replay_buffer_data = NULL; atomic_store(&self->finished, 0); pthread_mutex_lock(&encoder->replay_mutex); self->cloned_replay_buffer = gsr_replay_buffer_clone(encoder->replay_buffer); - if(self->cloned_replay_buffer && restart_replay) { - self->restarted_replay_buffer_data = gsr_replay_buffer_take_data(encoder->replay_buffer); - if(!self->restarted_replay_buffer_data) - gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_replay_save: failed to restart the replay buffer"); - } pthread_mutex_unlock(&encoder->replay_mutex); if(!self->cloned_replay_buffer) { /* TODO: Return this error to mark the replay as failed */ diff --git a/src/replay_buffer/replay_buffer.c b/src/replay_buffer/replay_buffer.c index 4958bba..2f7db54 100644 --- a/src/replay_buffer/replay_buffer.c +++ b/src/replay_buffer/replay_buffer.c @@ -47,10 +47,6 @@ gsr_replay_buffer* gsr_replay_buffer_clone(gsr_replay_buffer *self) { return self->clone(self); } -gsr_replay_buffer* gsr_replay_buffer_take_data(gsr_replay_buffer *self) { - return self->take_data(self); -} - gsr_replay_buffer_iterator gsr_replay_buffer_find_packet_index_by_time_passed(gsr_replay_buffer *self, int seconds) { return self->find_packet_index_by_time_passed(self, seconds); } diff --git a/src/replay_buffer/replay_buffer_disk.c b/src/replay_buffer/replay_buffer_disk.c index da82df5..b61ccfa 100644 --- a/src/replay_buffer/replay_buffer_disk.c +++ b/src/replay_buffer/replay_buffer_disk.c @@ -254,36 +254,6 @@ static uint8_t* gsr_replay_buffer_disk_iterator_get_packet_data(gsr_replay_buffe return packet_data; } -static gsr_replay_buffer* gsr_replay_buffer_disk_take_data(gsr_replay_buffer *replay_buffer) { - gsr_replay_buffer_disk *self = (gsr_replay_buffer_disk*)replay_buffer; - gsr_replay_buffer_disk *destination = calloc(1, sizeof(gsr_replay_buffer_disk)); - if(!destination) - return NULL; - - gsr_replay_buffer_disk_set_impl_funcs(destination); - - destination->replay_buffer_time = self->replay_buffer_time; - destination->storage_counter = self->storage_counter; - destination->storage_num_bytes_written = self->storage_num_bytes_written; - destination->storage_fd = self->storage_fd; - - for(size_t i = 0; i < self->num_files; ++i) { - destination->files[i] = self->files[i]; - } - destination->num_files = self->num_files; - - snprintf(destination->replay_directory, sizeof(destination->replay_directory), "%s", self->replay_directory); - /* |self| keeps writing to the same directory, so it's the one that removes it */ - destination->owns_directory = false; - - self->num_files = 0; - self->storage_fd = 0; - self->storage_num_bytes_written = 0; - /* |storage_counter| is not reset, to not overwrite the files that were moved to |destination| */ - - return (gsr_replay_buffer*)destination; -} - static gsr_replay_buffer* gsr_replay_buffer_disk_clone(gsr_replay_buffer *replay_buffer) { gsr_replay_buffer_disk *self = (gsr_replay_buffer_disk*)replay_buffer; gsr_replay_buffer_disk *destination = calloc(1, sizeof(gsr_replay_buffer_disk)); @@ -430,7 +400,6 @@ static void gsr_replay_buffer_disk_set_impl_funcs(gsr_replay_buffer_disk *self) self->replay_buffer.iterator_get_packet = gsr_replay_buffer_disk_iterator_get_packet; self->replay_buffer.iterator_get_packet_data = gsr_replay_buffer_disk_iterator_get_packet_data; self->replay_buffer.clone = gsr_replay_buffer_disk_clone; - self->replay_buffer.take_data = gsr_replay_buffer_disk_take_data; self->replay_buffer.find_packet_index_by_time_passed = gsr_replay_buffer_disk_find_file_index_by_time_passed; self->replay_buffer.find_keyframe = gsr_replay_buffer_disk_find_keyframe; self->replay_buffer.iterator_next = gsr_replay_buffer_disk_iterator_next; diff --git a/src/replay_buffer/replay_buffer_ram.c b/src/replay_buffer/replay_buffer_ram.c index ce3914b..39f31b1 100644 --- a/src/replay_buffer/replay_buffer_ram.c +++ b/src/replay_buffer/replay_buffer_ram.c @@ -130,32 +130,6 @@ static uint8_t* gsr_replay_buffer_ram_iterator_get_packet_data(gsr_replay_buffer return NULL; } -static gsr_replay_buffer* gsr_replay_buffer_ram_take_data(gsr_replay_buffer *replay_buffer) { - gsr_replay_buffer_ram *self = (gsr_replay_buffer_ram*)replay_buffer; - gsr_av_packet_ram **packets = calloc(self->capacity_num_packets, sizeof(gsr_av_packet_ram*)); - if(!packets) - return NULL; - - gsr_replay_buffer_ram *destination = calloc(1, sizeof(gsr_replay_buffer_ram)); - if(!destination) { - free(packets); - return NULL; - } - - gsr_replay_buffer_ram_set_impl_funcs(destination); - - destination->packets = self->packets; - destination->capacity_num_packets = self->capacity_num_packets; - destination->num_packets = self->num_packets; - destination->index = self->index; - - self->packets = packets; - self->num_packets = 0; - self->index = 0; - - return (gsr_replay_buffer*)destination; -} - static gsr_replay_buffer* gsr_replay_buffer_ram_clone(gsr_replay_buffer *replay_buffer) { gsr_replay_buffer_ram *self = (gsr_replay_buffer_ram*)replay_buffer; gsr_replay_buffer_ram *destination = calloc(1, sizeof(gsr_replay_buffer_ram)); @@ -242,7 +216,6 @@ static void gsr_replay_buffer_ram_set_impl_funcs(gsr_replay_buffer_ram *self) { self->replay_buffer.iterator_get_packet = gsr_replay_buffer_ram_iterator_get_packet; self->replay_buffer.iterator_get_packet_data = gsr_replay_buffer_ram_iterator_get_packet_data; self->replay_buffer.clone = gsr_replay_buffer_ram_clone; - self->replay_buffer.take_data = gsr_replay_buffer_ram_take_data; self->replay_buffer.find_packet_index_by_time_passed = gsr_replay_buffer_ram_find_packet_index_by_time_passed; self->replay_buffer.find_keyframe = gsr_replay_buffer_ram_find_keyframe; self->replay_buffer.iterator_next = gsr_replay_buffer_ram_iterator_next; -- cgit v1.2.3 From fcaf4b5444a5eea6ba70694770b7e0b6ba86728b Mon Sep 17 00:00:00 2001 From: dec05eba Date: Fri, 7 Aug 2026 13:54:08 +0200 Subject: Faster replay ram clear by not free'ing the data immediately --- src/replay_buffer/replay_buffer_ram.c | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/replay_buffer/replay_buffer_ram.c b/src/replay_buffer/replay_buffer_ram.c index 39f31b1..a148a20 100644 --- a/src/replay_buffer/replay_buffer_ram.c +++ b/src/replay_buffer/replay_buffer_ram.c @@ -95,14 +95,14 @@ static bool gsr_replay_buffer_ram_append(gsr_replay_buffer *replay_buffer, const return true; } +/* + The packets are not free'd here because that takes seconds when the replay buffer holds several gigabytes of data, + which would block the thread that appends to the replay buffer for that long. The packets are instead left in place + where they are no longer reachable and gsr_replay_buffer_ram_append free's them one by one as it overwrites them, + which it already does when the ring buffer wraps around. +*/ static void gsr_replay_buffer_ram_clear(gsr_replay_buffer *replay_buffer) { gsr_replay_buffer_ram *self = (gsr_replay_buffer_ram*)replay_buffer; - for(size_t i = 0; i < self->num_packets; ++i) { - if(self->packets[i]) { - gsr_av_packet_ram_unref(self->packets[i]); - self->packets[i] = NULL; - } - } self->num_packets = 0; self->index = 0; } -- cgit v1.2.3 From 4eafd8f5cf00fb72f3b481a429ecd068972fb390 Mon Sep 17 00:00:00 2001 From: dec05eba Date: Sat, 8 Aug 2026 01:16:52 +0200 Subject: Remove unmaintained distro packages --- README.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/README.md b/README.md index 9715748..56cd972 100644 --- a/README.md +++ b/README.md @@ -43,14 +43,11 @@ If you still prefer to install GPU Screen Recorder with a package manager instea Here are some known unofficial packages: * Alpine Linux: [gpu-screen-recorder](https://pkgs.alpinelinux.org/package/edge/testing/x86/gpu-screen-recorder) * AppImage [AppImage GitHub releases](https://github.com/pkgforge-dev/gpu-screen-recorder-AppImage/releases) -* CentOS: [Copr](https://copr.fedorainfracloud.org/coprs/brycensranch/gpu-screen-recorder-git/) * Debian: [gpu-screen-recorder-cli](https://tracker.debian.org/pkg/gpu-screen-recorder) or [gpu-screen-recorder alternative repository](https://sekoohaka.ddns.net/?p=about#debian-repository) -* Fedora: [Copr](https://copr.fedorainfracloud.org/coprs/brycensranch/gpu-screen-recorder-git/) * Nix: [NixOS wiki](https://wiki.nixos.org/wiki/Gpu-screen-recorder) * Nobara: [Nobara wiki](https://wiki.nobaraproject.org/en/general-usage/additional-software/GPU-Screen-Recorder) * OpenMandriva: [gpu-screen-recorder](https://github.com/OpenMandrivaAssociation/gpu-screen-recorder) * openSUSE: [openSUSE software repository](https://software.opensuse.org/package/gpu-screen-recorder) -* Solus: [gpu-screen-recorder](https://github.com/getsolus/packages/tree/main/packages/g/gpu-screen-recorder) * Ubuntu: [gpu-screen-recorder-cli](https://launchpad.net/ubuntu/+source/gpu-screen-recorder) * Void Linux: [gpu-screen-recorder](https://github.com/cherrybtw/gsr4xbps) -- cgit v1.2.3 From 89e87050a4a567ff9e07ca4e4c454adf9a1a2c65 Mon Sep 17 00:00:00 2001 From: dec05eba Date: Sat, 8 Aug 2026 03:14:52 +0200 Subject: Fix crash with vulkan encoding --- src/encoder/video/vulkan.c | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/src/encoder/video/vulkan.c b/src/encoder/video/vulkan.c index f2872d0..c90a1f9 100644 --- a/src/encoder/video/vulkan.c +++ b/src/encoder/video/vulkan.c @@ -37,6 +37,7 @@ typedef struct { PFN_vkResetFences vkResetFences; PFN_vkWaitForFences vkWaitForFences; PFN_vkGetDeviceQueue vkGetDeviceQueue; + PFN_vkGetDeviceQueue2 vkGetDeviceQueue2; PFN_vkQueueSubmit vkQueueSubmit; PFN_vkResetCommandBuffer vkResetCommandBuffer; PFN_vkCreateSemaphore vkCreateSemaphore; @@ -101,6 +102,7 @@ static bool gsr_vk_funcs_load(gsr_vk_funcs *vk, PFN_vkGetInstanceProcAddr get_in LOAD_DEV(vkResetFences) LOAD_DEV(vkWaitForFences) LOAD_DEV(vkGetDeviceQueue) + LOAD_DEV(vkGetDeviceQueue2) LOAD_DEV(vkQueueSubmit) LOAD_DEV(vkResetCommandBuffer) LOAD_DEV(vkCreateSemaphore) @@ -162,6 +164,16 @@ static AVVulkanDeviceContext* video_codec_context_get_vulkan_data(AVCodecContext return (AVVulkanDeviceContext*)device_context->hwctx; } +/* The flags that FFmpeg created the device queues with, which vkGetDeviceQueue2 has to be given to return the queue */ +static VkDeviceQueueCreateFlags get_device_queue_create_flags(AVVulkanDeviceContext *vv) { +#if LIBAVUTIL_VERSION_INT >= AV_VERSION_INT(61, 1, 100) + return vv->queue_flags; +#else + (void)vv; + return 0; +#endif +} + static int get_graphics_queue_family(AVVulkanDeviceContext *vv) { #if LIBAVUTIL_VERSION_INT >= AV_VERSION_INT(59, 39, 100) for(int i = 0; i < vv->nb_qf; i++) { @@ -393,7 +405,22 @@ static bool gsr_video_encoder_vulkan_setup_textures(gsr_video_encoder_vulkan *se return false; } - self->vk.vkGetDeviceQueue(vv->act_dev, (uint32_t)get_graphics_queue_family(vv), 0, &self->vk_queue); + /* + The queues have to be retrieved with vkGetDeviceQueue2 because vkGetDeviceQueue only works for queues that + were created without flags, and FFmpeg creates them with VK_DEVICE_QUEUE_CREATE_INTERNALLY_SYNCHRONIZED_BIT_KHR + when the driver supports it. vkGetDeviceQueue returns a NULL queue in that case, which crashes when it's used. + */ + VkDeviceQueueInfo2 queue_info = { + .sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_INFO_2, + .flags = get_device_queue_create_flags(vv), + .queueFamilyIndex = (uint32_t)get_graphics_queue_family(vv), + .queueIndex = 0, + }; + self->vk.vkGetDeviceQueue2(vv->act_dev, &queue_info, &self->vk_queue); + if(!self->vk_queue) { + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_video_encoder_vulkan_setup_textures: vkGetDeviceQueue2 failed"); + return false; + } /* Transition export images UNDEFINED → GENERAL so GL can use them */ VkCommandBufferBeginInfo begin_info = { -- cgit v1.2.3 From f4d1d3d2b00513c9323ae76a6d52117ee2dd0392 Mon Sep 17 00:00:00 2001 From: dec05eba Date: Sat, 8 Aug 2026 03:36:47 +0200 Subject: Update ffmpeg static to 9.0 --- subprojects/ffmpeg.wrap | 10 +-- .../ffmpeg-mbedtls-default-ca-certs.patch | 6 +- .../ffmpeg-mbedtls-skip-non-dtls-packets.patch | 48 ------------ .../ffmpeg-nvenc-runtime-api-version.patch | 90 ++++++++++------------ subprojects/packagefiles/ffmpeg/meson.build | 2 +- 5 files changed, 51 insertions(+), 105 deletions(-) delete mode 100644 subprojects/packagefiles/ffmpeg-mbedtls-skip-non-dtls-packets.patch diff --git a/subprojects/ffmpeg.wrap b/subprojects/ffmpeg.wrap index c98e19a..d086138 100644 --- a/subprojects/ffmpeg.wrap +++ b/subprojects/ffmpeg.wrap @@ -1,7 +1,7 @@ [wrap-file] -directory = ffmpeg-8.1 -source_url = https://ffmpeg.org/releases/ffmpeg-8.1.tar.xz -source_filename = ffmpeg-8.1.tar.xz -source_hash = b072aed6871998cce9b36e7774033105ca29e33632be5b6347f3206898e0756a +directory = ffmpeg-9.0 +source_url = https://ffmpeg.org/releases/ffmpeg-9.0.tar.xz +source_filename = ffmpeg-9.0.tar.xz +source_hash = 7f607a00dd0d28a729d5a4811205812eef01cf6ef6155025febb6f36a9062d52 patch_directory = ffmpeg -diff_files = ffmpeg-nvenc-runtime-api-version.patch, ffmpeg-mbedtls-skip-non-dtls-packets.patch, ffmpeg-mbedtls-default-ca-certs.patch +diff_files = ffmpeg-nvenc-runtime-api-version.patch, ffmpeg-mbedtls-default-ca-certs.patch diff --git a/subprojects/packagefiles/ffmpeg-mbedtls-default-ca-certs.patch b/subprojects/packagefiles/ffmpeg-mbedtls-default-ca-certs.patch index c2c4c3f..23e48f1 100644 --- a/subprojects/packagefiles/ffmpeg-mbedtls-default-ca-certs.patch +++ b/subprojects/packagefiles/ffmpeg-mbedtls-default-ca-certs.patch @@ -1,9 +1,9 @@ --- a/libavformat/tls_mbedtls.c +++ b/libavformat/tls_mbedtls.c -@@ -42,6 +42,65 @@ +@@ -43,6 +43,65 @@ + #include "libavutil/parseutils.h" #include "libavutil/avstring.h" #include "libavutil/random_seed.h" - #include "libavutil/intreadwrite.h" +#include "libavutil/getenv_utf8.h" + +/* @@ -66,7 +66,7 @@ static int mbedtls_x509_fingerprint(char *cert_buf, size_t cert_sz, char **fingerprint) { -@@ -557,6 +617,9 @@ +@@ -555,6 +614,9 @@ av_log(h, AV_LOG_ERROR, "mbedtls_x509_crt_parse_file for CA cert returned %d\n", ret); goto fail; } diff --git a/subprojects/packagefiles/ffmpeg-mbedtls-skip-non-dtls-packets.patch b/subprojects/packagefiles/ffmpeg-mbedtls-skip-non-dtls-packets.patch deleted file mode 100644 index 4f18644..0000000 --- a/subprojects/packagefiles/ffmpeg-mbedtls-skip-non-dtls-packets.patch +++ /dev/null @@ -1,48 +0,0 @@ ---- a/libavformat/tls_mbedtls.c -+++ b/libavformat/tls_mbedtls.c -@@ -41,6 +41,7 @@ - #include "libavutil/parseutils.h" - #include "libavutil/avstring.h" - #include "libavutil/random_seed.h" -+#include "libavutil/intreadwrite.h" - - static int mbedtls_x509_fingerprint(char *cert_buf, size_t cert_sz, char **fingerprint) - { -@@ -376,6 +377,27 @@ - return handle_transport_error(h, "ffurl_write", MBEDTLS_ERR_SSL_WANT_WRITE, ret); - } - -+/* -+ * Some webrtc servers, such as the ones based on pion, send stun packets during the -+ * dtls handshake. openssl and gnutls filter those out internally but mbedtls passes -+ * every received udp packet to its dtls state machine, which makes the handshake fail. -+ * This is the same check as the one in whip.c, which ffmpeg n8.1 doesn't share yet. -+ */ -+#define DTLS_CONTENT_TYPE_CHANGE_CIPHER_SPEC 20 -+#define DTLS_RECORD_LAYER_HEADER_LEN 13 -+#define DTLS_VERSION_10 0xfeff -+#define DTLS_VERSION_12 0xfefd -+ -+static int is_dtls_packet(const unsigned char *buf, int size) -+{ -+ if (size > DTLS_RECORD_LAYER_HEADER_LEN) { -+ uint16_t version = AV_RB16(&buf[1]); -+ return buf[0] >= DTLS_CONTENT_TYPE_CHANGE_CIPHER_SPEC && -+ (version == DTLS_VERSION_10 || version == DTLS_VERSION_12); -+ } -+ return 0; -+} -+ - static int mbedtls_recv(void *ctx, unsigned char *buf, size_t len) - { - TLSContext *tls_ctx = (TLSContext*) ctx; -@@ -394,6 +416,9 @@ - } - av_log(tls_ctx, AV_LOG_TRACE, "Set UDP remote addr on UDP socket, now 'connected'\n"); - } -+ /* Skip non-DTLS packets such as STUN to avoid failures. */ -+ if (shr->is_dtls && !is_dtls_packet(buf, ret)) -+ return MBEDTLS_ERR_SSL_WANT_READ; - return ret; - } - if (h->max_packet_size && len > h->max_packet_size) diff --git a/subprojects/packagefiles/ffmpeg-nvenc-runtime-api-version.patch b/subprojects/packagefiles/ffmpeg-nvenc-runtime-api-version.patch index 53c8419..eb3bd5f 100644 --- a/subprojects/packagefiles/ffmpeg-nvenc-runtime-api-version.patch +++ b/subprojects/packagefiles/ffmpeg-nvenc-runtime-api-version.patch @@ -1,12 +1,10 @@ -diff --git a/libavcodec/nvenc.c b/libavcodec/nvenc.c -index 392230526d..ba26580acb 100644 --- a/libavcodec/nvenc.c +++ b/libavcodec/nvenc.c -@@ -263,7 +263,15 @@ static void nvenc_map_preset(NvencContext *ctx) +@@ -235,7 +235,15 @@ static void nvenc_print_driver_requirement(AVCodecContext *avctx, int level) { --#if NVENCAPI_CHECK_VERSION(13, 1) +-#if NVENCAPI_CHECK_VERSION(13, 2) +/* With runtime API version negotiation the build-time header version no + * longer dictates the required driver; the runtime floor is API 11.1. */ +#if 1 @@ -15,11 +13,11 @@ index 392230526d..ba26580acb 100644 +# else + const char *minver = "470.57.02"; +# endif -+#elif NVENCAPI_CHECK_VERSION(13, 1) ++#elif NVENCAPI_CHECK_VERSION(13, 2) const char *minver = "(unknown)"; - #elif NVENCAPI_CHECK_VERSION(13, 0) - const char *minver = "570.0"; -@@ -347,6 +355,193 @@ static void nvenc_print_driver_requirement(AVCodecContext *avctx, int level) + #elif NVENCAPI_CHECK_VERSION(13, 1) + const char *minver = "610.00"; +@@ -279,6 +287,193 @@ #define to_nv_color_trc(n) (uint32_t)(n) #endif @@ -213,7 +211,7 @@ index 392230526d..ba26580acb 100644 static av_cold int nvenc_load_libraries(AVCodecContext *avctx) { NvencContext *ctx = avctx->priv_data; -@@ -371,16 +566,35 @@ static av_cold int nvenc_load_libraries(AVCodecContext *avctx) +@@ -303,16 +498,35 @@ av_log(avctx, AV_LOG_VERBOSE, "Loaded Nvenc version %d.%d\n", nvenc_max_ver >> 4, nvenc_max_ver & 0xf); @@ -252,7 +250,7 @@ index 392230526d..ba26580acb 100644 err = dl_fn->nvenc_dl->NvEncodeAPICreateInstance(&dl_fn->nvenc_funcs); if (err != NV_ENC_SUCCESS) -@@ -421,8 +635,8 @@ static av_cold int nvenc_open_session(AVCodecContext *avctx) +@@ -353,8 +567,8 @@ NV_ENCODE_API_FUNCTION_LIST *p_nvenc = &ctx->nvenc_dload_funcs.nvenc_funcs; NVENCSTATUS ret; @@ -263,7 +261,7 @@ index 392230526d..ba26580acb 100644 if (ctx->d3d11_device) { params.device = ctx->d3d11_device; params.deviceType = NV_ENC_DEVICE_TYPE_DIRECTX; -@@ -483,7 +697,7 @@ static int nvenc_check_cap(AVCodecContext *avctx, NV_ENC_CAPS cap) +@@ -415,7 +629,7 @@ NV_ENC_CAPS_PARAM params = { 0 }; int ret, val = 0; @@ -272,7 +270,7 @@ index 392230526d..ba26580acb 100644 params.capsToQuery = cap; ret = p_nvenc->nvEncGetEncodeCaps(ctx->nvencoder, ctx->init_encode_params.encodeGUID, ¶ms, &val); -@@ -498,6 +712,15 @@ static int nvenc_check_capabilities(AVCodecContext *avctx) +@@ -430,6 +644,15 @@ NvencContext *ctx = avctx->priv_data; int tmp, ret; @@ -288,7 +286,7 @@ index 392230526d..ba26580acb 100644 ret = nvenc_check_codec_support(avctx); if (ret < 0) { av_log(avctx, AV_LOG_WARNING, "Codec not supported\n"); -@@ -1219,7 +1442,10 @@ static av_cold int nvenc_setup_rate_control(AVCodecContext *avctx) +@@ -1088,7 +1311,10 @@ ctx->encode_config.rcParams.lookaheadDepth, ctx->rc_lookahead); #ifdef NVENC_HAVE_LOOKAHEAD_LEVEL @@ -300,7 +298,7 @@ index 392230526d..ba26580acb 100644 switch (ctx->lookahead_level) { case NV_ENC_LOOKAHEAD_LEVEL_0: case NV_ENC_LOOKAHEAD_LEVEL_1: -@@ -1413,8 +1639,12 @@ static av_cold int nvenc_setup_h264_config(AVCodecContext *avctx) +@@ -1274,8 +1500,12 @@ h264->level = ctx->level; #ifdef NVENC_HAVE_NEW_BIT_DEPTH_API @@ -315,7 +313,7 @@ index 392230526d..ba26580acb 100644 #endif if (ctx->coder >= 0) -@@ -1450,6 +1680,13 @@ static av_cold int nvenc_setup_h264_config(AVCodecContext *avctx) +@@ -1307,6 +1537,13 @@ #endif #ifdef NVENC_HAVE_TIME_CODE @@ -329,7 +327,7 @@ index 392230526d..ba26580acb 100644 if (ctx->s12m_tc) h264->enableTimeCode = 1; #endif -@@ -1501,7 +1738,8 @@ static av_cold int nvenc_setup_hevc_config(AVCodecContext *avctx) +@@ -1358,18 +1595,21 @@ hevc->intraRefreshCnt = cc->gopLength - 1; cc->gopLength = NVENC_INFINITE_GOPLENGTH; #ifdef NVENC_HAVE_HEVC_OUTPUT_RECOVERY_POINT_SEI @@ -337,9 +335,7 @@ index 392230526d..ba26580acb 100644 + if (NVENC_API_AT_LEAST(ctx, 12, 0)) + hevc->outputRecoveryPointSEI = 1; #endif - #ifdef NVENC_HAVE_SINGLE_SLICE_INTRA_REFRESH hevc->singleSliceIntraRefresh = ctx->single_slice_intra_refresh; -@@ -1509,12 +1747,14 @@ static av_cold int nvenc_setup_hevc_config(AVCodecContext *avctx) } #ifdef NVENC_HAVE_HEVC_AND_AV1_MASTERING_METADATA @@ -359,8 +355,8 @@ index 392230526d..ba26580acb 100644 + } #endif - #ifdef NVENC_HAVE_HEVC_CONSTRAINED_ENCODING -@@ -1613,8 +1853,18 @@ static av_cold int nvenc_setup_hevc_config(AVCodecContext *avctx) + if (ctx->constrained_encoding) +@@ -1464,8 +1704,18 @@ hevc->chromaFormatIDC = IS_YUV444(ctx->data_pix_fmt) ? 3 : IS_YUV422(ctx->data_pix_fmt) ? 2 : 1; #ifdef NVENC_HAVE_NEW_BIT_DEPTH_API @@ -381,7 +377,7 @@ index 392230526d..ba26580acb 100644 #else hevc->pixelBitDepthMinus8 = IS_10BIT(ctx->data_pix_fmt) ? 2 : 0; #endif -@@ -1722,20 +1972,28 @@ static av_cold int nvenc_setup_av1_config(AVCodecContext *avctx) +@@ -1569,20 +1819,28 @@ av1->chromaFormatIDC = IS_YUV444(ctx->data_pix_fmt) ? 3 : 1; #ifdef NVENC_HAVE_NEW_BIT_DEPTH_API @@ -418,7 +414,7 @@ index 392230526d..ba26580acb 100644 #endif if (ctx->b_ref_mode >= 0) -@@ -1829,16 +2087,16 @@ static av_cold int nvenc_setup_encoder(AVCodecContext *avctx) +@@ -1676,16 +1934,16 @@ int res = 0; int dw, dh; @@ -437,9 +433,9 @@ index 392230526d..ba26580acb 100644 + preset_config.version = ctx->api_vers->preset_config; + preset_config.presetCfg.version = ctx->api_vers->config; - #ifdef NVENC_HAVE_NEW_PRESETS ctx->init_encode_params.tuningInfo = ctx->tuning_info; -@@ -1848,6 +2106,16 @@ static av_cold int nvenc_setup_encoder(AVCodecContext *avctx) + +@@ -1694,6 +1952,16 @@ else if (ctx->flags & NVENC_LOWLATENCY) ctx->init_encode_params.tuningInfo = NV_ENC_TUNING_INFO_LOW_LATENCY; @@ -456,7 +452,7 @@ index 392230526d..ba26580acb 100644 nv_status = p_nvenc->nvEncGetEncodePresetConfigEx(ctx->nvencoder, ctx->init_encode_params.encodeGUID, ctx->init_encode_params.presetGUID, -@@ -1864,7 +2132,7 @@ static av_cold int nvenc_setup_encoder(AVCodecContext *avctx) +@@ -1704,7 +1972,7 @@ memcpy(&ctx->encode_config, &preset_config.presetCfg, sizeof(ctx->encode_config)); @@ -465,7 +461,7 @@ index 392230526d..ba26580acb 100644 compute_dar(avctx, &dw, &dh); ctx->init_encode_params.darHeight = dh; -@@ -1897,11 +2165,16 @@ static av_cold int nvenc_setup_encoder(AVCodecContext *avctx) +@@ -1735,11 +2003,16 @@ ctx->init_encode_params.enableWeightedPrediction = 1; #ifdef NVENC_HAVE_SPLIT_FRAME_ENCODING @@ -486,7 +482,7 @@ index 392230526d..ba26580acb 100644 } #endif -@@ -2042,7 +2315,7 @@ static av_cold int nvenc_alloc_surface(AVCodecContext *avctx, int idx) +@@ -1881,7 +2154,7 @@ NVENCSTATUS nv_status; NV_ENC_CREATE_BITSTREAM_BUFFER allocOut = { 0 }; @@ -495,7 +491,7 @@ index 392230526d..ba26580acb 100644 if (avctx->pix_fmt == AV_PIX_FMT_CUDA || avctx->pix_fmt == AV_PIX_FMT_D3D11) { ctx->surfaces[idx].in_ref = av_frame_alloc(); -@@ -2058,7 +2331,7 @@ static av_cold int nvenc_alloc_surface(AVCodecContext *avctx, int idx) +@@ -1897,7 +2170,7 @@ return AVERROR(EINVAL); } @@ -504,7 +500,7 @@ index 392230526d..ba26580acb 100644 allocSurf.width = avctx->width; allocSurf.height = avctx->height; allocSurf.bufferFmt = ctx->surfaces[idx].format; -@@ -2146,7 +2419,7 @@ static av_cold int nvenc_setup_extradata(AVCodecContext *avctx) +@@ -1985,7 +2258,7 @@ char tmpHeader[NV_MAX_SEQ_HDR_LEN]; NV_ENC_SEQUENCE_PARAM_PAYLOAD payload = { 0 }; @@ -513,7 +509,7 @@ index 392230526d..ba26580acb 100644 payload.spsppsBuffer = tmpHeader; payload.inBufferSize = sizeof(tmpHeader); -@@ -2178,7 +2451,7 @@ av_cold int ff_nvenc_encode_close(AVCodecContext *avctx) +@@ -2017,7 +2290,7 @@ /* the encoder has to be flushed before it can be closed */ if (ctx->nvencoder) { @@ -522,7 +518,7 @@ index 392230526d..ba26580acb 100644 .encodePicFlags = NV_ENC_PIC_FLAG_EOS }; res = nvenc_push_context(avctx); -@@ -2402,7 +2675,7 @@ static int nvenc_register_frame(AVCodecContext *avctx, const AVFrame *frame) +@@ -2241,7 +2514,7 @@ if (idx < 0) return idx; @@ -531,7 +527,7 @@ index 392230526d..ba26580acb 100644 reg.width = frames_ctx->width; reg.height = frames_ctx->height; reg.pitch = frame->linesize[0]; -@@ -2457,7 +2730,7 @@ static int nvenc_upload_frame(AVCodecContext *avctx, const AVFrame *frame, +@@ -2296,7 +2569,7 @@ return res; if (!ctx->registered_frames[reg_idx].mapped) { @@ -540,7 +536,7 @@ index 392230526d..ba26580acb 100644 ctx->registered_frames[reg_idx].in_map.registeredResource = ctx->registered_frames[reg_idx].regptr; nv_status = p_nvenc->nvEncMapInputResource(ctx->nvencoder, &ctx->registered_frames[reg_idx].in_map); if (nv_status != NV_ENC_SUCCESS) { -@@ -2477,7 +2750,7 @@ static int nvenc_upload_frame(AVCodecContext *avctx, const AVFrame *frame, +@@ -2316,7 +2589,7 @@ } else { NV_ENC_LOCK_INPUT_BUFFER lockBufferParams = { 0 }; @@ -549,7 +545,7 @@ index 392230526d..ba26580acb 100644 lockBufferParams.inputBuffer = nvenc_frame->input_surface; nv_status = p_nvenc->nvEncLockInputBuffer(ctx->nvencoder, &lockBufferParams); -@@ -2725,31 +2998,38 @@ static int process_output_surface(AVCodecContext *avctx, AVPacket *pkt, NvencSur +@@ -2569,31 +2842,38 @@ NvencDynLoadFunctions *dl_fn = &ctx->nvenc_dload_funcs; NV_ENCODE_API_FUNCTION_LIST *p_nvenc = &dl_fn->nvenc_funcs; @@ -595,7 +591,7 @@ index 392230526d..ba26580acb 100644 nv_status = p_nvenc->nvEncUnlockBitstream(ctx->nvencoder, tmpoutsurf->output_surface); if (nv_status != NV_ENC_SUCCESS) { -@@ -2776,7 +3056,7 @@ static int process_output_surface(AVCodecContext *avctx, AVPacket *pkt, NvencSur +@@ -2620,7 +2900,7 @@ tmpoutsurf->input_surface = NULL; } @@ -603,8 +599,8 @@ index 392230526d..ba26580acb 100644 + switch (lock_params->pictureType) { case NV_ENC_PIC_TYPE_IDR: pkt->flags |= AV_PKT_FLAG_KEY; - case NV_ENC_PIC_TYPE_I: -@@ -2799,13 +3079,13 @@ static int process_output_surface(AVCodecContext *avctx, AVPacket *pkt, NvencSur + av_fallthrough; +@@ -2644,13 +2924,13 @@ } ff_encode_add_stats_side_data(pkt, @@ -621,7 +617,7 @@ index 392230526d..ba26580acb 100644 if (res < 0) goto error2; -@@ -2948,14 +3228,21 @@ static void reconfig_encoder(AVCodecContext *avctx, const AVFrame *frame) +@@ -2793,14 +3073,21 @@ NV_ENCODE_API_FUNCTION_LIST *p_nvenc = &ctx->nvenc_dload_funcs.nvenc_funcs; NVENCSTATUS ret; @@ -646,7 +642,7 @@ index 392230526d..ba26580acb 100644 compute_dar(avctx, &dw, &dh); if (dw != ctx->init_encode_params.darWidth || dh != ctx->init_encode_params.darHeight) { -@@ -2964,47 +3251,59 @@ static void reconfig_encoder(AVCodecContext *avctx, const AVFrame *frame) +@@ -2809,47 +3096,59 @@ ctx->init_encode_params.darWidth, ctx->init_encode_params.darHeight, dw, dh); @@ -717,7 +713,7 @@ index 392230526d..ba26580acb 100644 needs_encode_config = 1; needs_reconfig = 1; -@@ -3012,10 +3311,10 @@ static void reconfig_encoder(AVCodecContext *avctx, const AVFrame *frame) +@@ -2857,10 +3156,10 @@ } if (!needs_encode_config) @@ -730,7 +726,7 @@ index 392230526d..ba26580acb 100644 if (ret != NV_ENC_SUCCESS) { nvenc_print_error(avctx, ret, "failed to reconfigure nvenc"); } else { -@@ -3025,9 +3324,9 @@ static void reconfig_encoder(AVCodecContext *avctx, const AVFrame *frame) +@@ -2870,9 +3169,9 @@ } if (reconfig_bitrate) { @@ -743,7 +739,7 @@ index 392230526d..ba26580acb 100644 } } -@@ -3126,7 +3425,7 @@ static int nvenc_send_frame(AVCodecContext *avctx, const AVFrame *frame) +@@ -2971,7 +3270,7 @@ NV_ENCODE_API_FUNCTION_LIST *p_nvenc = &dl_fn->nvenc_funcs; NV_ENC_PIC_PARAMS pic_params = { 0 }; @@ -752,12 +748,10 @@ index 392230526d..ba26580acb 100644 if ((!ctx->cu_context && !ctx->d3d11_device) || !ctx->nvencoder) return AVERROR(EINVAL); -diff --git a/libavcodec/nvenc.h b/libavcodec/nvenc.h -index 069ba82bc9..4225c612aa 100644 --- a/libavcodec/nvenc.h +++ b/libavcodec/nvenc.h -@@ -111,6 +111,54 @@ typedef void ID3D11Device; - #define NVENC_HAVE_MVHEVC +@@ -80,6 +80,54 @@ + #define NVENC_HAVE_AV1_HGOP_SUPPORT #endif +/* @@ -811,7 +805,7 @@ index 069ba82bc9..4225c612aa 100644 typedef struct NvencSurface { NV_ENC_INPUT_PTR input_surface; -@@ -216,6 +264,11 @@ typedef struct NvencContext +@@ -172,6 +220,11 @@ NvencDynLoadFunctions nvenc_dload_funcs; NV_ENC_INITIALIZE_PARAMS init_encode_params; @@ -823,7 +817,7 @@ index 069ba82bc9..4225c612aa 100644 NV_ENC_CONFIG encode_config; CUcontext cu_context; CUcontext cu_context_internal; -@@ -321,6 +374,11 @@ typedef struct NvencContext +@@ -275,6 +328,11 @@ int cbr_padding; int multiview, multiview_supported; int display_sei_sent; diff --git a/subprojects/packagefiles/ffmpeg/meson.build b/subprojects/packagefiles/ffmpeg/meson.build index 96345f2..11973f1 100644 --- a/subprojects/packagefiles/ffmpeg/meson.build +++ b/subprojects/packagefiles/ffmpeg/meson.build @@ -1,4 +1,4 @@ -project('ffmpeg', 'c', version : '8.1') +project('ffmpeg', 'c', version : '9.0') # Neither ffmpeg nor the libraries it depends on use meson, so they are built with their # own build systems into a shared prefix and then exposed to gpu-screen-recorder as -- cgit v1.2.3 From 65c75169276be632ed8543a1f0c03d966f683b19 Mon Sep 17 00:00:00 2001 From: dec05eba Date: Sat, 8 Aug 2026 04:02:34 +0200 Subject: Fix nvenc crash on shutdown --- src/recorder/recorder.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/recorder/recorder.c b/src/recorder/recorder.c index 901077c..ca4dcea 100644 --- a/src/recorder/recorder.c +++ b/src/recorder/recorder.c @@ -911,6 +911,9 @@ void gsr_recorder_destroy(gsr_recorder *self, bool exiting) { if(self->color_conversion_initialized) gsr_color_conversion_deinit(&self->color_conversion); + if(self->video_frame) + av_frame_free(&self->video_frame); + if(self->video_encoder) gsr_video_encoder_destroy(self->video_encoder, self->video_codec_context); @@ -919,9 +922,6 @@ void gsr_recorder_destroy(gsr_recorder *self, bool exiting) { gsr_video_sources_deinit(&self->video_sources_data); - if(self->video_frame) - av_frame_free(&self->video_frame); - if(self->video_codec_context) avcodec_free_context(&self->video_codec_context); -- cgit v1.2.3 From 20740fe57962c9cbfb0cd6c2f1036da775c40c8b Mon Sep 17 00:00:00 2001 From: dec05eba Date: Sat, 8 Aug 2026 04:11:11 +0200 Subject: Fix nvenc crash on shutdown. part 2 --- src/encoder/video/nvenc.c | 2 +- src/encoder/video/vaapi.c | 2 +- src/encoder/video/vulkan.c | 2 +- src/recorder/recorder.c | 8 ++++---- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/encoder/video/nvenc.c b/src/encoder/video/nvenc.c index f7fca41..594416c 100644 --- a/src/encoder/video/nvenc.c +++ b/src/encoder/video/nvenc.c @@ -152,7 +152,7 @@ void gsr_video_encoder_nvenc_stop(gsr_video_encoder_nvenc *self, AVCodecContext self->target_textures[0] = 0; self->target_textures[1] = 0; - if(video_codec_context->hw_frames_ctx) + if(video_codec_context && video_codec_context->hw_frames_ctx) av_buffer_unref(&video_codec_context->hw_frames_ctx); if(self->device_ctx) av_buffer_unref(&self->device_ctx); diff --git a/src/encoder/video/vaapi.c b/src/encoder/video/vaapi.c index d310125..12dd3d7 100644 --- a/src/encoder/video/vaapi.c +++ b/src/encoder/video/vaapi.c @@ -294,7 +294,7 @@ void gsr_video_encoder_vaapi_stop(gsr_video_encoder_vaapi *self, AVCodecContext self->target_textures[0] = 0; self->target_textures[1] = 0; - if(video_codec_context->hw_frames_ctx) + if(video_codec_context && video_codec_context->hw_frames_ctx) av_buffer_unref(&video_codec_context->hw_frames_ctx); if(self->device_ctx) av_buffer_unref(&self->device_ctx); diff --git a/src/encoder/video/vulkan.c b/src/encoder/video/vulkan.c index c90a1f9..232a37d 100644 --- a/src/encoder/video/vulkan.c +++ b/src/encoder/video/vulkan.c @@ -862,7 +862,7 @@ void gsr_video_encoder_vulkan_stop(gsr_video_encoder_vulkan *self, AVCodecContex } } - if(video_codec_context->hw_frames_ctx) + if(video_codec_context && video_codec_context->hw_frames_ctx) av_buffer_unref(&video_codec_context->hw_frames_ctx); if(self->device_ctx) av_buffer_unref(&self->device_ctx); diff --git a/src/recorder/recorder.c b/src/recorder/recorder.c index ca4dcea..29d36d8 100644 --- a/src/recorder/recorder.c +++ b/src/recorder/recorder.c @@ -914,17 +914,17 @@ void gsr_recorder_destroy(gsr_recorder *self, bool exiting) { if(self->video_frame) av_frame_free(&self->video_frame); + if(self->video_codec_context) + avcodec_free_context(&self->video_codec_context); + if(self->video_encoder) - gsr_video_encoder_destroy(self->video_encoder, self->video_codec_context); + gsr_video_encoder_destroy(self->video_encoder, NULL); if(self->encoder_initialized) gsr_encoder_deinit(&self->encoder, exiting); gsr_video_sources_deinit(&self->video_sources_data); - if(self->video_codec_context) - avcodec_free_context(&self->video_codec_context); - if(self->av_format_context) { if(self->av_format_context->pb && !(self->av_format_context->oformat->flags & AVFMT_NOFILE)) avio_close(self->av_format_context->pb); -- cgit v1.2.3 From 80517840f4ae64469721598e8373d81f2e4493dd Mon Sep 17 00:00:00 2001 From: dec05eba Date: Sat, 8 Aug 2026 04:18:58 +0200 Subject: 6.0.0 --- meson.build | 2 +- project.conf | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/meson.build b/meson.build index 0f7ded6..5a514fe 100644 --- a/meson.build +++ b/meson.build @@ -1,4 +1,4 @@ -project('gpu-screen-recorder', 'c', version : '5.15.3', default_options : ['c_std=gnu11', 'warning_level=2']) +project('gpu-screen-recorder', 'c', version : '6.0.0', default_options : ['c_std=gnu11', 'warning_level=2']) add_project_arguments('-Wshadow', language : 'c') if get_option('buildtype') == 'debug' diff --git a/project.conf b/project.conf index e3895a2..47e993b 100644 --- a/project.conf +++ b/project.conf @@ -1,7 +1,7 @@ [package] name = "gpu-screen-recorder" type = "executable" -version = "5.15.3" +version = "6.0.0" platforms = ["posix"] [config] -- cgit v1.2.3