aboutsummaryrefslogtreecommitdiffhomepage
path: root/src
diff options
context:
space:
mode:
authordec05eba <dec05eba@protonmail.com>2026-08-01 23:08:48 +0200
committerdec05eba <dec05eba@protonmail.com>2026-08-01 23:08:48 +0200
commit9dda64c39d0fdd6409305faff33c217dcf161e60 (patch)
treeb66b522a0af8d0730bef7852cbe9e3b78835fa86 /src
parent40b6a13d14680cfacb3cdb59f04224540b80d9e4 (diff)
Move screenshot capture and CLI commands from main.cpp into separate modules
The list commands return exit codes through args_parser instead of exiting.
Diffstat (limited to 'src')
-rw-r--r--src/args_parser.c68
-rw-r--r--src/cli/commands.c372
-rw-r--r--src/main.cpp581
-rw-r--r--src/recorder/screenshot.c221
4 files changed, 665 insertions, 577 deletions
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 <stdio.h>
+#include <string.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <sys/wait.h>
+
+#ifndef GSR_VERSION
+#define GSR_VERSION "unknown"
+#endif
+
+static void list_system_info(bool wayland) {
+ printf("display_server|%s\n", wayland ? "wayland" : "x11");
+ bool supports_app_audio = false;
+#ifdef GSR_APP_AUDIO
+ supports_app_audio = pulseaudio_server_is_pipewire();
+ if(supports_app_audio) {
+ gsr_pipewire_audio audio;
+ if(gsr_pipewire_audio_init(&audio))
+ gsr_pipewire_audio_deinit(&audio);
+ else
+ supports_app_audio = false;
+ }
+#endif
+ printf("supports_app_audio|%s\n", supports_app_audio ? "yes" : "no");
+}
+
+static void list_gpu_info(gsr_egl *egl) {
+ switch(egl->gpu_info.vendor) {
+ case GSR_GPU_VENDOR_AMD:
+ printf("vendor|amd\n");
+ break;
+ case GSR_GPU_VENDOR_INTEL:
+ printf("vendor|intel\n");
+ break;
+ case GSR_GPU_VENDOR_NVIDIA:
+ printf("vendor|nvidia\n");
+ break;
+ case GSR_GPU_VENDOR_BROADCOM:
+ printf("vendor|broadcom\n");
+ break;
+ }
+ 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 <string.h>
+#include <assert.h>
+#include <unistd.h>
+
+#include <X11/Xlib.h>
+
+#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;
+ }
+}