aboutsummaryrefslogtreecommitdiffhomepage
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/args_parser.c354
-rw-r--r--src/capture/kms.c28
-rw-r--r--src/capture/nvfbc.c42
-rw-r--r--src/capture/portal.c53
-rw-r--r--src/capture/v4l2.c74
-rw-r--r--src/capture/xcomposite.c17
-rw-r--r--src/capture/ximage.c14
-rw-r--r--src/cli/commands.c366
-rw-r--r--src/cli/ipc.c962
-rw-r--r--src/cli/main.c708
-rw-r--r--src/codec_query/nvenc.c26
-rw-r--r--src/codec_query/vaapi.c10
-rw-r--r--src/codec_query/vulkan.c27
-rw-r--r--src/color_conversion.c47
-rw-r--r--src/cuda.c15
-rw-r--r--src/cursor.c4
-rw-r--r--src/damage.c28
-rw-r--r--src/dbus.c115
-rw-r--r--src/egl.c44
-rw-r--r--src/encoder/encoder.c37
-rw-r--r--src/encoder/video/nvenc.c20
-rw-r--r--src/encoder/video/software.c7
-rw-r--r--src/encoder/video/vaapi.c40
-rw-r--r--src/encoder/video/vulkan.c69
-rw-r--r--src/ffmpeg_utils.c40
-rw-r--r--src/image_writer.c8
-rw-r--r--src/json.c69
-rw-r--r--src/kde_night_light.c5
-rw-r--r--src/library_loader.c4
-rw-r--r--src/log.c47
-rw-r--r--src/main.cpp4734
-rw-r--r--src/pipewire_audio.c45
-rw-r--r--src/pipewire_video.c68
-rw-r--r--src/plugins.c24
-rw-r--r--src/recorder/audio_capture.c610
-rw-r--r--src/recorder/audio_codec.c219
-rw-r--r--src/recorder/audio_input.c336
-rw-r--r--src/recorder/capture_setup.c571
-rw-r--r--src/recorder/capture_source.c390
-rw-r--r--src/recorder/codec_select.c526
-rw-r--r--src/recorder/muxer.c268
-rw-r--r--src/recorder/recorder.c969
-rw-r--r--src/recorder/recording_clock.c60
-rw-r--r--src/recorder/replay_save.c209
-rw-r--r--src/recorder/screenshot.c221
-rw-r--r--src/recorder/video_codec.c428
-rw-r--r--src/recorder/windowing.c140
-rw-r--r--src/replay_buffer/replay_buffer.c7
-rw-r--r--src/replay_buffer/replay_buffer_disk.c20
-rw-r--r--src/replay_buffer/replay_buffer_ram.c19
-rw-r--r--src/shader.c12
-rw-r--r--src/sound.c (renamed from src/sound.cpp)166
-rw-r--r--src/utils.c106
-rw-r--r--src/wayland_host_bridge.c7
-rw-r--r--src/window/wayland.c17
-rw-r--r--src/window/window.c5
-rw-r--r--src/window/x11.c6
57 files changed, 8014 insertions, 5449 deletions
diff --git a/src/args_parser.c b/src/args_parser.c
index c37712a..d10da67 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;
}
}
@@ -204,7 +206,7 @@ static void usage_header(void) {
"[-cursor yes|no] [-keyint <value>] [-restore-portal-session yes|no] [-portal-session-token-filepath filepath] [-encoder gpu|cpu] "
"[-fallback-cpu-encoding yes|no] [-o <output_file>] [-ro <output_directory>] [-ffmpeg-opts <options>] [--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 <socket_path>] [-v yes|no] [-gl-debug yes|no] [-exclude-metadata yes|no] [--version] [-h|--help]\n", program_name);
fflush(stdout);
}
@@ -251,283 +253,283 @@ 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) {
- 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");
- self->audio_codec = GSR_AUDIO_CODEC_OPUS;
+ 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->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] == '/') {
- 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);
+ 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)) {
- fprintf(stderr, "gsr error: Script \"%s\" either doesn't exist or it's not a file\n", 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)) {
- 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->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) {
- 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);
+ 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) {
- fprintf(stderr, "gsr error: -q is expected to be 0 or larger, got %" PRIi64 "\n", 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 {
- 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;
}
}
- 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) {
- fprintf(stderr, "gsr error: invalid value for option -s '%s', expected a value in format WxH\n", output_resolution_str);
+ 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) {
- 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);
+ 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) {
- fprintf(stderr, "gsr error: invalid value for option -region '%s', expected a value in format WxH+X+Y\n", region_str);
+ 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) {
- fprintf(stderr, "gsr error: invalid value for option -region '%s', expected width and height to be greater or equal to 0\n", region_str);
+ 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) {
- fprintf(stderr, "gsr error: replay mode is not applicable to live streaming\n");
+ 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) {
- 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->settings.filename);
return false;
}
}
} else {
- if(!self->container_format) {
- fprintf(stderr, "gsr error: option -c is required when using option -r\n");
+ 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)) {
- fprintf(stderr, "gsr error: File \"%s\" exists but it's not a directory\n", 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 {
- 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");
+ 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)) {
- fprintf(stderr, "gsr warning: -write-first-frame-ts is ignored for livestreaming or when output is piped\n");
- self->write_first_frame_ts = false;
+ 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->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) {
- fprintf(stderr, "gsr warning: live stream detected, -sc script is ignored\n");
- self->recording_saved_script = NULL;
+ if(self->settings.is_livestream && self->settings.recording_saved_script) {
+ gsr_log(GSR_LOG_LEVEL_WARNING, "live stream detected, -sc script is ignored");
+ 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;
}
-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 {
- fprintf(stderr, "gsr error: expected --list-capture-options to be called with either no extra arguments or 1 extra argument (card path)\n");
- return false;
+ 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 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;
@@ -569,27 +571,28 @@ bool args_parser_parse(args_parser *self, int argc, char **argv, const args_hand
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) {
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;
+ return ARGS_PARSE_RESULT_ERROR;
}
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;
+ return ARGS_PARSE_RESULT_ERROR;
}
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;
+ return ARGS_PARSE_RESULT_ERROR;
}
const char *arg_value = argv[i + 1];
@@ -603,80 +606,80 @@ 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;
+ return ARGS_PARSE_RESULT_ERROR;
}
break;
}
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;
+ return ARGS_PARSE_RESULT_ERROR;
}
break;
}
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;
+ return ARGS_PARSE_RESULT_ERROR;
}
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;
+ return ARGS_PARSE_RESULT_ERROR;
}
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;
+ return ARGS_PARSE_RESULT_ERROR;
}
break;
}
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;
+ return ARGS_PARSE_RESULT_ERROR;
}
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;
+ return ARGS_PARSE_RESULT_ERROR;
}
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;
+ return ARGS_PARSE_RESULT_ERROR;
}
break;
}
}
if(!arg_append_value(arg, arg_value)) {
- fprintf(stderr, "gsr error: failed to append argument, out of memory\n");
- return false;
+ gsr_log(GSR_LOG_LEVEL_ERROR, "failed to append argument, out of memory");
+ return ARGS_PARSE_RESULT_ERROR;
}
}
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;
+ 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) {
@@ -688,34 +691,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) {
- fprintf(stderr, "gsr warning: qp bitrate mode is not supported on Steam Deck because of Steam Deck driver bugs. Using vbr instead\n");
- self->bitrate_mode = GSR_BITRATE_MODE_VBR;
+ 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->settings.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");
- self->bitrate_mode = GSR_BITRATE_MODE_QP;
+ 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->settings.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;
+ 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) {
- fprintf(stderr, "gsr info: your gpu appears to be very old (older than maxwell architecture). Switching to lower preset\n");
- self->very_old_gpu = true;
+ gsr_log(GSR_LOG_LEVEL_INFO, "your gpu appears to be very old (older than maxwell architecture). Switching to lower preset");
+ self->settings.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));
+ 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/capture/kms.c b/src/capture/kms.c
index 0f7c37c..2285fb6 100644
--- a/src/capture/kms.c
+++ b/src/capture/kms.c
@@ -1,13 +1,12 @@
#include "../../include/capture/kms.h"
+#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 <stdlib.h>
#include <string.h>
-#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
@@ -159,7 +158,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 +190,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 +453,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 +467,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 +611,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 +634,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 +820,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 +847,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 +1101,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..8f386d4 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"
@@ -8,7 +9,6 @@
#include <dlfcn.h>
#include <stdlib.h>
#include <string.h>
-#include <stdio.h>
#include <math.h>
#include <assert.h>
@@ -74,13 +74,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 +89,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 +143,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 +155,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 +172,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 +215,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 +226,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 +256,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 +271,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 +349,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 +385,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 +414,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..ace04b9 100644
--- a/src/capture/xcomposite.c
+++ b/src/capture/xcomposite.c
@@ -1,14 +1,11 @@
#include "../../include/capture/xcomposite.h"
+#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 <stdlib.h>
-#include <stdio.h>
-#include <string.h>
-#include <assert.h>
#include <X11/Xlib.h>
@@ -63,7 +60,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 +72,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 +86,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 +126,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 +150,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 +288,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..d80cd0b 100644
--- a/src/capture/ximage.c
+++ b/src/capture/ximage.c
@@ -1,13 +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 <stdlib.h>
-#include <stdio.h>
#include <string.h>
-#include <assert.h>
#include <X11/Xlib.h>
@@ -39,7 +37,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 +59,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 +93,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 +184,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/cli/commands.c b/src/cli/commands.c
new file mode 100644
index 0000000..9b77f79
--- /dev/null
+++ b/src/cli/commands.c
@@ -0,0 +1,366 @@
+#include "../../include/cli/commands.h"
+#include "../../include/recorder/windowing.h"
+#include "../../include/recorder/codec_select.h"
+#include "../../include/recorder/error.h"
+#include "../../include/capture/v4l2.h"
+#include "../../include/window/window.h"
+#include "../../include/sound.h"
+#include "../../include/utils.h"
+#include "../../include/log.h"
+#ifdef GSR_APP_AUDIO
+#include "../../include/pipewire_audio.h"
+#endif
+#ifdef GSR_PORTAL
+#include "../../include/dbus.h"
+#endif
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <sys/wait.h>
+
+#ifndef GSR_VERSION
+#define GSR_VERSION "unknown"
+#endif
+
+static void list_system_info(bool wayland) {
+ printf("display_server|%s\n", wayland ? "wayland" : "x11");
+ bool supports_app_audio = false;
+#ifdef GSR_APP_AUDIO
+ supports_app_audio = pulseaudio_server_is_pipewire();
+ if(supports_app_audio) {
+ gsr_pipewire_audio audio;
+ if(gsr_pipewire_audio_init(&audio))
+ gsr_pipewire_audio_deinit(&audio);
+ else
+ supports_app_audio = false;
+ }
+#endif
+ printf("supports_app_audio|%s\n", supports_app_audio ? "yes" : "no");
+}
+
+static void list_gpu_info(gsr_egl *egl) {
+ switch(egl->gpu_info.vendor) {
+ case GSR_GPU_VENDOR_AMD:
+ printf("vendor|amd\n");
+ break;
+ case GSR_GPU_VENDOR_INTEL:
+ printf("vendor|intel\n");
+ break;
+ case GSR_GPU_VENDOR_NVIDIA:
+ printf("vendor|nvidia\n");
+ break;
+ case GSR_GPU_VENDOR_BROADCOM:
+ printf("vendor|broadcom\n");
+ break;
+ case GSR_GPU_VENDOR_APPLE:
+ printf("vendor|apple\n");
+ break;
+ }
+ printf("card_path|%s\n", egl->card_path);
+}
+
+static void list_supported_video_codecs(gsr_egl *egl, bool wayland) {
+ // Dont clean it up on purpose to increase shutdown speed
+ gsr_supported_video_codecs supported_video_codecs;
+ get_supported_video_codecs(egl, GSR_VIDEO_CODEC_H264, false, false, &supported_video_codecs);
+
+ gsr_supported_video_codecs supported_video_codecs_vulkan;
+ get_supported_video_codecs(egl, GSR_VIDEO_CODEC_H264_VULKAN, false, false, &supported_video_codecs_vulkan);
+
+ set_supported_video_codecs_ffmpeg(&supported_video_codecs, &supported_video_codecs_vulkan, egl->gpu_info.vendor);
+
+ if(supported_video_codecs.h264.supported)
+ puts("h264");
+ if(avcodec_find_encoder_by_name("libx264"))
+ puts("h264_software");
+ if(supported_video_codecs.hevc.supported)
+ puts("hevc");
+ if(supported_video_codecs.hevc_hdr.supported && wayland)
+ puts("hevc_hdr");
+ if(supported_video_codecs.hevc_10bit.supported)
+ puts("hevc_10bit");
+ if(supported_video_codecs.av1.supported)
+ puts("av1");
+ if(supported_video_codecs.av1_hdr.supported && wayland)
+ puts("av1_hdr");
+ if(supported_video_codecs.av1_10bit.supported)
+ puts("av1_10bit");
+ if(supported_video_codecs.vp8.supported)
+ puts("vp8");
+ if(supported_video_codecs.vp9.supported)
+ puts("vp9");
+ if(supported_video_codecs_vulkan.h264.supported)
+ puts("h264_vulkan");
+ if(supported_video_codecs_vulkan.hevc.supported)
+ puts("hevc_vulkan");
+ if(supported_video_codecs_vulkan.hevc_hdr.supported && wayland)
+ puts("hevc_hdr_vulkan");
+ if(supported_video_codecs_vulkan.hevc_10bit.supported)
+ puts("hevc_10bit_vulkan");
+ if(supported_video_codecs_vulkan.av1.supported)
+ puts("av1_vulkan");
+ if(supported_video_codecs_vulkan.av1_hdr.supported && wayland)
+ puts("av1_hdr_vulkan");
+ if(supported_video_codecs_vulkan.av1_10bit.supported)
+ puts("av1_10bit_vulkan");
+}
+
+void run_recording_saved_script_async(const char *script_file, const char *video_file, const char *type) {
+ char script_file_full[PATH_MAX];
+ script_file_full[0] = '\0';
+ if(!realpath(script_file, script_file_full)) {
+ gsr_log(GSR_LOG_LEVEL_ERROR, "script file not found: %s", script_file);
+ return;
+ }
+
+ const char *args[7];
+ const bool inside_flatpak = getenv("FLATPAK_ID") != NULL;
+
+ if(inside_flatpak) {
+ args[0] = "flatpak-spawn";
+ args[1] = "--host";
+ args[2] = "--";
+ args[3] = script_file_full;
+ args[4] = video_file;
+ args[5] = type;
+ args[6] = NULL;
+ } else {
+ args[0] = script_file_full;
+ args[1] = video_file;
+ args[2] = type;
+ args[3] = NULL;
+ }
+
+ pid_t pid = fork();
+ if(pid == -1) {
+ perror(script_file_full);
+ return;
+ } else if(pid == 0) { // child
+ setsid();
+ signal(SIGHUP, SIG_IGN);
+
+ pid_t second_child = fork();
+ if(second_child == 0) { // child
+ execvp(args[0], (char* const*)args);
+ perror(script_file_full);
+ _exit(127);
+ } else if(second_child != -1) { // parent
+ _exit(0);
+ }
+ } else { // parent
+ waitpid(pid, NULL, 0);
+ }
+}
+
+typedef struct {
+ const gsr_window *window;
+ int num_monitors;
+} capture_options_callback;
+
+static void output_monitor_info(const gsr_monitor *monitor, void *userdata) {
+ capture_options_callback *options = (capture_options_callback*)userdata;
+ if(gsr_window_get_display_server(options->window) == GSR_DISPLAY_SERVER_WAYLAND) {
+ vec2i monitor_size = monitor->size;
+ gsr_monitor_rotation monitor_rotation = GSR_MONITOR_ROT_0;
+ vec2i monitor_position = {0, 0};
+ drm_monitor_get_display_server_data(options->window, monitor, &monitor_rotation, &monitor_position);
+ if(monitor_rotation == GSR_MONITOR_ROT_90 || monitor_rotation == GSR_MONITOR_ROT_270)
+ {
+ const int tmp = monitor_size.x;
+ monitor_size.x = monitor_size.y;
+ monitor_size.y = tmp;
+ }
+ printf("%.*s|%dx%d\n", monitor->name_len, monitor->name, monitor_size.x, monitor_size.y);
+ } else {
+ printf("%.*s|%dx%d\n", monitor->name_len, monitor->name, monitor->size.x, monitor->size.y);
+ }
+ ++options->num_monitors;
+}
+
+static void camera_query_callback(const char *path, const gsr_capture_v4l2_supported_setup *setup, void *userdata) {
+ (void)userdata;
+ printf("%s|%ux%u@%uhz|%s\n", path, setup->resolution.width, setup->resolution.height, gsr_capture_v4l2_framerate_to_number(setup->framerate), gsr_capture_v4l2_pixfmt_to_string(setup->pixfmt));
+}
+
+static int list_monitors(const gsr_window *window, const char *card_path) {
+ capture_options_callback options;
+ options.window = window;
+ options.num_monitors = 0;
+
+ const bool is_x11 = gsr_window_get_display_server(window) == GSR_DISPLAY_SERVER_X11;
+ const gsr_connection_type connection_type = is_x11 ? GSR_CONNECTION_X11 : GSR_CONNECTION_DRM;
+ for_each_active_monitor_output(window, card_path, connection_type, output_monitor_info, &options);
+
+ return options.num_monitors;
+}
+
+static void list_supported_capture_options(const gsr_window *window, const char *card_path, bool do_list_monitors) {
+ const bool wayland = gsr_window_get_display_server(window) == GSR_DISPLAY_SERVER_WAYLAND;
+ if(!wayland) {
+ puts("window");
+ puts("focused");
+ }
+
+ int num_monitors = 0;
+ if(do_list_monitors)
+ num_monitors = list_monitors(window, card_path);
+
+ if(num_monitors > 0)
+ puts("region");
+
+ gsr_capture_v4l2_list_devices(camera_query_callback, NULL);
+
+#ifdef GSR_PORTAL
+ // Desktop portal capture on x11 doesn't seem to be hardware accelerated
+ if(!wayland)
+ return;
+
+ gsr_dbus dbus;
+ if(!gsr_dbus_init(&dbus, NULL))
+ return;
+
+ char *session_handle = NULL;
+ if(gsr_dbus_screencast_create_session(&dbus, &session_handle) == 0)
+ puts("portal");
+
+ gsr_dbus_deinit(&dbus);
+#endif
+}
+
+int version_command(void *userdata) {
+ (void)userdata;
+ puts(GSR_VERSION);
+ fflush(stdout);
+ return 0;
+}
+
+int info_command(void *userdata) {
+ (void)userdata;
+ gsr_windowing windowing;
+ const gsr_windowing_params windowing_params = { .monitor_capture = true };
+ if(gsr_windowing_init(&windowing, &windowing_params) != GSR_ERROR_OK)
+ return 1;
+
+ if(gsr_windowing_load_egl(&windowing, &windowing_params) != GSR_ERROR_OK) {
+ gsr_windowing_deinit(&windowing);
+ return 22;
+ }
+
+ const bool wayland = gsr_windowing_is_wayland(&windowing);
+
+ av_log_set_level(AV_LOG_FATAL);
+
+ puts("section=system_info");
+ list_system_info(wayland);
+ if(windowing.egl.gpu_info.is_steam_deck)
+ puts("is_steam_deck|yes");
+ else
+ puts("is_steam_deck|no");
+ printf("gsr_version|%s\n", GSR_VERSION);
+ puts("section=gpu_info");
+ list_gpu_info(&windowing.egl);
+ puts("section=video_codecs");
+ list_supported_video_codecs(&windowing.egl, wayland);
+ puts("section=image_formats");
+ puts("jpeg");
+ puts("png");
+ puts("section=capture_options");
+ list_supported_capture_options(windowing.window, windowing.egl.card_path, windowing.card_path_found);
+
+ fflush(stdout);
+ gsr_windowing_deinit(&windowing);
+ return 0;
+}
+
+int list_audio_devices_command(void *userdata) {
+ (void)userdata;
+ gsr_audio_devices audio_devices;
+ get_pulseaudio_inputs(&audio_devices);
+
+ if(audio_devices.default_output[0] != '\0')
+ puts("default_output|Default output");
+
+ if(audio_devices.default_input[0] != '\0')
+ puts("default_input|Default input");
+
+ for(size_t i = 0; i < audio_devices.num_items; ++i) {
+ printf("%s|%s\n", audio_devices.items[i].name, audio_devices.items[i].description);
+ }
+
+ gsr_audio_devices_deinit(&audio_devices);
+ fflush(stdout);
+ return 0;
+}
+
+static bool app_audio_query_callback(const char *app_name, void *userdata) {
+ (void)userdata;
+ puts(app_name);
+ return true;
+}
+
+int list_application_audio_command(void *userdata) {
+ (void)userdata;
+#ifdef GSR_APP_AUDIO
+ if(pulseaudio_server_is_pipewire()) {
+ gsr_pipewire_audio audio;
+ if(gsr_pipewire_audio_init(&audio)) {
+ gsr_pipewire_audio_for_each_app(&audio, app_audio_query_callback, NULL);
+ gsr_pipewire_audio_deinit(&audio);
+ }
+ }
+#endif
+
+ fflush(stdout);
+ return 0;
+}
+
+int list_v4l2_devices(void *userdata) {
+ (void)userdata;
+ gsr_capture_v4l2_list_devices(camera_query_callback, NULL);
+
+ fflush(stdout);
+ return 0;
+}
+
+int list_capture_options_command(const char *card_path, void *userdata) {
+ (void)userdata;
+ gsr_windowing windowing;
+ const gsr_windowing_params windowing_params = { .monitor_capture = true };
+ if(gsr_windowing_init(&windowing, &windowing_params) != GSR_ERROR_OK)
+ return 1;
+
+ if(!card_path && gsr_windowing_load_egl(&windowing, &windowing_params) != GSR_ERROR_OK) {
+ gsr_windowing_deinit(&windowing);
+ return 22;
+ }
+
+ if(card_path)
+ list_supported_capture_options(windowing.window, card_path, true);
+ else
+ list_supported_capture_options(windowing.window, windowing.egl.card_path, windowing.card_path_found);
+
+ fflush(stdout);
+ gsr_windowing_deinit(&windowing);
+ return 0;
+}
+
+int list_monitors_command(void *userdata) {
+ (void)userdata;
+ gsr_windowing windowing;
+ const gsr_windowing_params windowing_params = { .monitor_capture = true };
+ if(gsr_windowing_init(&windowing, &windowing_params) != GSR_ERROR_OK)
+ return 1;
+
+ if(gsr_windowing_load_egl(&windowing, &windowing_params) != GSR_ERROR_OK) {
+ gsr_windowing_deinit(&windowing);
+ return 22;
+ }
+
+ if(windowing.card_path_found)
+ list_monitors(windowing.window, windowing.egl.card_path);
+
+ fflush(stdout);
+ gsr_windowing_deinit(&windowing);
+ return 0;
+}
diff --git a/src/cli/ipc.c b/src/cli/ipc.c
new file mode 100644
index 0000000..2fb3000
--- /dev/null
+++ b/src/cli/ipc.c
@@ -0,0 +1,962 @@
+#include "../../include/cli/ipc.h"
+#include "../../include/recorder/error.h"
+#include "../../include/recorder/replay_save.h"
+#include "../../include/json.h"
+#include "../../include/log.h"
+
+#include <stdio.h>
+#include <string.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <inttypes.h>
+#include <limits.h>
+#include <poll.h>
+#include <signal.h>
+#include <unistd.h>
+#include <sys/socket.h>
+#include <sys/stat.h>
+#include <sys/un.h>
+
+#ifdef __linux__
+#include <sys/epoll.h>
+#else
+#include <sys/event.h>
+#include <sys/time.h>
+#endif
+
+#define GSR_IPC_MAX_REQUEST_NAME_SIZE 64
+#define GSR_IPC_MAX_EVENTS (2 + GSR_IPC_MAX_CLIENTS*2)
+#define GSR_IPC_SHUTDOWN_SEND_TIMEOUT_MILLISECONDS 1000
+#define GSR_IPC_SOCKET_MODE 0600
+
+#define GSR_IPC_WAKEUP_QUIT (1 << 0)
+#define GSR_IPC_WAKEUP_COMPLETED_REQUEST (1 << 1)
+
+typedef struct {
+ int64_t id;
+ char name[GSR_IPC_MAX_REQUEST_NAME_SIZE];
+ sj_Value data;
+ bool has_data;
+ const char *request_end;
+} gsr_ipc_request;
+
+typedef struct {
+ int fd;
+ bool readable;
+ bool writable;
+} gsr_ipc_event;
+
+static bool string_is_only_whitespace(const char *str, size_t size) {
+ for(size_t i = 0; i < size; ++i) {
+ if(str[i] != ' ' && str[i] != '\t' && str[i] != '\r' && str[i] != '\n')
+ return false;
+ }
+ return true;
+}
+
+static bool fd_set_cloexec(int fd) {
+ const int flags = fcntl(fd, F_GETFD);
+ return flags != -1 && fcntl(fd, F_SETFD, flags | FD_CLOEXEC) != -1;
+}
+
+static bool fd_set_nonblocking(int fd) {
+ const int flags = fcntl(fd, F_GETFL);
+ return flags != -1 && fcntl(fd, F_SETFL, flags | O_NONBLOCK) != -1;
+}
+
+#ifdef __linux__
+static bool ipc_poller_init(gsr_ipc *self) {
+ self->poll_fd = epoll_create1(EPOLL_CLOEXEC);
+ if(self->poll_fd == -1) {
+ gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_ipc: failed to create an epoll instance, error: %s", strerror(errno));
+ return false;
+ }
+ return true;
+}
+
+static bool ipc_poller_add(gsr_ipc *self, int fd) {
+ struct epoll_event event;
+ memset(&event, 0, sizeof(event));
+ event.events = EPOLLIN | EPOLLET;
+ event.data.fd = fd;
+ return epoll_ctl(self->poll_fd, EPOLL_CTL_ADD, fd, &event) == 0;
+}
+
+static bool ipc_poller_set_write_notify(gsr_ipc *self, int fd, bool enable) {
+ struct epoll_event event;
+ memset(&event, 0, sizeof(event));
+ event.events = EPOLLIN | EPOLLET | (enable ? EPOLLOUT : 0);
+ event.data.fd = fd;
+ return epoll_ctl(self->poll_fd, EPOLL_CTL_MOD, fd, &event) == 0;
+}
+
+/* Returns the number of events, or -1 on failure. Waits until at least one event is available */
+static int ipc_poller_wait(gsr_ipc *self, gsr_ipc_event *events, int events_capacity) {
+ struct epoll_event platform_events[GSR_IPC_MAX_EVENTS];
+ if(events_capacity > GSR_IPC_MAX_EVENTS)
+ events_capacity = GSR_IPC_MAX_EVENTS;
+
+ const int num_events = epoll_wait(self->poll_fd, platform_events, events_capacity, -1);
+ if(num_events == -1)
+ return errno == EINTR ? 0 : -1;
+
+ for(int i = 0; i < num_events; ++i) {
+ events[i].fd = platform_events[i].data.fd;
+ events[i].readable = platform_events[i].events & (EPOLLIN | EPOLLHUP | EPOLLERR);
+ events[i].writable = platform_events[i].events & EPOLLOUT;
+ }
+ return num_events;
+}
+#else
+static bool ipc_poller_init(gsr_ipc *self) {
+ self->poll_fd = kqueue();
+ if(self->poll_fd == -1) {
+ gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_ipc: failed to create a kqueue instance, error: %s", strerror(errno));
+ return false;
+ }
+ fd_set_cloexec(self->poll_fd);
+ return true;
+}
+
+static bool ipc_poller_add(gsr_ipc *self, int fd) {
+ struct kevent change;
+ EV_SET(&change, fd, EVFILT_READ, EV_ADD | EV_CLEAR, 0, 0, NULL);
+ return kevent(self->poll_fd, &change, 1, NULL, 0, NULL) != -1;
+}
+
+static bool ipc_poller_set_write_notify(gsr_ipc *self, int fd, bool enable) {
+ struct kevent change;
+ EV_SET(&change, fd, EVFILT_WRITE, enable ? (EV_ADD | EV_CLEAR) : EV_DELETE, 0, 0, NULL);
+ return kevent(self->poll_fd, &change, 1, NULL, 0, NULL) != -1;
+}
+
+/* Returns the number of events, or -1 on failure. Waits until at least one event is available */
+static int ipc_poller_wait(gsr_ipc *self, gsr_ipc_event *events, int events_capacity) {
+ struct kevent platform_events[GSR_IPC_MAX_EVENTS];
+ if(events_capacity > GSR_IPC_MAX_EVENTS)
+ events_capacity = GSR_IPC_MAX_EVENTS;
+
+ const int num_events = kevent(self->poll_fd, NULL, 0, platform_events, events_capacity, NULL);
+ if(num_events == -1)
+ return errno == EINTR ? 0 : -1;
+
+ for(int i = 0; i < num_events; ++i) {
+ events[i].fd = platform_events[i].ident;
+ events[i].readable = platform_events[i].filter == EVFILT_READ;
+ events[i].writable = platform_events[i].filter == EVFILT_WRITE;
+ }
+ return num_events;
+}
+#endif
+
+/* Only used when the ipc thread exits, to not lose replies that haven't been fully sent yet */
+static bool ipc_send_all_blocking(int fd, const char *data, size_t size) {
+ size_t offset = 0;
+ while(offset < size) {
+ const ssize_t bytes_written = send(fd, data + offset, size - offset, MSG_NOSIGNAL);
+ if(bytes_written > 0) {
+ offset += bytes_written;
+ continue;
+ }
+
+ if(bytes_written == -1 && errno == EINTR)
+ continue;
+
+ if(bytes_written == -1 && (errno == EAGAIN || errno == EWOULDBLOCK)) {
+ struct pollfd poll_fd;
+ poll_fd.fd = fd;
+ poll_fd.events = POLLOUT;
+ poll_fd.revents = 0;
+
+ const int poll_result = poll(&poll_fd, 1, GSR_IPC_SHUTDOWN_SEND_TIMEOUT_MILLISECONDS);
+ if(poll_result == -1 && errno == EINTR)
+ continue;
+
+ if(poll_result <= 0)
+ return false;
+
+ continue;
+ }
+
+ return false;
+ }
+ return true;
+}
+
+static bool ipc_client_send_data(gsr_ipc *self, gsr_ipc_client *client, const char *data, size_t size) {
+ size_t offset = 0;
+ if(client->send_buffer_size == 0) {
+ while(offset < size) {
+ const ssize_t bytes_written = send(client->fd, data + offset, size - offset, MSG_NOSIGNAL);
+ if(bytes_written > 0) {
+ offset += bytes_written;
+ continue;
+ }
+
+ if(bytes_written == -1 && errno == EINTR)
+ continue;
+
+ if(bytes_written == -1 && (errno == EAGAIN || errno == EWOULDBLOCK))
+ break;
+
+ return false;
+ }
+ }
+
+ const size_t bytes_remaining = size - offset;
+ if(bytes_remaining == 0)
+ return true;
+
+ if(client->send_buffer_size + bytes_remaining > sizeof(client->send_buffer)) {
+ gsr_log(GSR_LOG_LEVEL_WARNING, "gsr_ipc: an ipc client isn't reading replies fast enough, disconnecting it");
+ return false;
+ }
+
+ const bool send_buffer_was_empty = client->send_buffer_size == 0;
+ memcpy(client->send_buffer + client->send_buffer_size, data + offset, bytes_remaining);
+ client->send_buffer_size += bytes_remaining;
+ return !send_buffer_was_empty || ipc_poller_set_write_notify(self, client->fd, true);
+}
+
+static bool ipc_client_flush_send_buffer(gsr_ipc *self, gsr_ipc_client *client) {
+ if(client->send_buffer_size == 0)
+ return true;
+
+ size_t offset = 0;
+ while(offset < client->send_buffer_size) {
+ const ssize_t bytes_written = send(client->fd, client->send_buffer + offset, client->send_buffer_size - offset, MSG_NOSIGNAL);
+ if(bytes_written > 0) {
+ offset += bytes_written;
+ continue;
+ }
+
+ if(bytes_written == -1 && errno == EINTR)
+ continue;
+
+ if(bytes_written == -1 && (errno == EAGAIN || errno == EWOULDBLOCK))
+ break;
+
+ return false;
+ }
+
+ memmove(client->send_buffer, client->send_buffer + offset, client->send_buffer_size - offset);
+ client->send_buffer_size -= offset;
+ return client->send_buffer_size != 0 || ipc_poller_set_write_notify(self, client->fd, false);
+}
+
+static bool ipc_client_send_reply(gsr_ipc *self, gsr_ipc_client *client, int64_t id, bool success, const char *error_message, const char *data) {
+ char reply[GSR_IPC_MAX_REPLY_SIZE];
+ int reply_size = 0;
+
+ if(success && data) {
+ char escaped_data[GSR_IPC_MAX_ESCAPED_DATA_SIZE];
+ gsr_json_escape_string(escaped_data, sizeof(escaped_data), data);
+ reply_size = snprintf(reply, sizeof(reply), "{\"id\":%" PRIi64 ",\"result\":\"ok\",\"data\":\"%s\"}\n", id, escaped_data);
+ } else if(success) {
+ reply_size = snprintf(reply, sizeof(reply), "{\"id\":%" PRIi64 ",\"result\":\"ok\"}\n", id);
+ } else {
+ char escaped_error_message[GSR_IPC_MAX_ESCAPED_ERROR_MESSAGE_SIZE];
+ gsr_json_escape_string(escaped_error_message, sizeof(escaped_error_message), error_message ? error_message : "");
+ reply_size = snprintf(reply, sizeof(reply), "{\"id\":%" PRIi64 ",\"result\":\"error\",\"data\":\"%s\"}\n", id, escaped_error_message);
+ }
+
+ if(reply_size < 0 || reply_size >= (int)sizeof(reply)) {
+ gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_ipc: failed to create a reply to request %" PRIi64, id);
+ return false;
+ }
+
+ return ipc_client_send_data(self, client, reply, reply_size);
+}
+
+static bool ipc_request_parse(char *data, size_t size, gsr_ipc_request *request, char *error_message, size_t error_message_size) {
+ memset(request, 0, sizeof(*request));
+ request->request_end = data + size;
+
+ sj_Reader reader = sj_reader(data, size);
+ const sj_Value root = sj_read(&reader);
+ if(root.type != SJ_OBJECT) {
+ snprintf(error_message, error_message_size, "expected the request to be a json object");
+ return false;
+ }
+
+ sj_Value id_value;
+ sj_Value name_value;
+ bool has_id = false;
+ bool has_name = false;
+
+ sj_Value key;
+ sj_Value value;
+ while(sj_iter_object(&reader, root, &key, &value)) {
+ if(gsr_json_string_equals(&key, "id")) {
+ id_value = value;
+ has_id = true;
+ } else if(gsr_json_string_equals(&key, "name")) {
+ name_value = value;
+ has_name = true;
+ } else if(gsr_json_string_equals(&key, "data")) {
+ request->data = value;
+ request->has_data = true;
+ }
+ }
+
+ if(reader.error) {
+ snprintf(error_message, error_message_size, "failed to parse the request: %s", reader.error);
+ return false;
+ }
+
+ if(!has_id) {
+ snprintf(error_message, error_message_size, "the request is missing the 'id' field");
+ return false;
+ }
+
+ if(!gsr_json_number_to_int64(&id_value, &request->id)) {
+ snprintf(error_message, error_message_size, "expected 'id' to be an integer");
+ return false;
+ }
+
+ if(!has_name) {
+ snprintf(error_message, error_message_size, "the request is missing the 'name' field");
+ return false;
+ }
+
+ if(name_value.type != SJ_STRING) {
+ snprintf(error_message, error_message_size, "expected 'name' to be a string");
+ return false;
+ }
+
+ snprintf(request->name, sizeof(request->name), "%.*s", (int)(name_value.end - name_value.start), name_value.start);
+ return true;
+}
+
+static bool json_value_to_save_replay_seconds(const sj_Value *value, int *seconds, char *error_message, size_t error_message_size) {
+ int64_t data_seconds = 0;
+ if(!gsr_json_number_to_int64(value, &data_seconds) || data_seconds <= 0 || data_seconds > INT_MAX) {
+ snprintf(error_message, error_message_size, "expected the number of seconds to save to be larger than 0");
+ return false;
+ }
+
+ *seconds = data_seconds;
+ return true;
+}
+
+static bool ipc_request_get_save_replay_options(const gsr_ipc_request *request, int *seconds, bool *has_restart_replay, bool *restart_replay, char *error_message, size_t error_message_size) {
+ *seconds = GSR_SAVE_REPLAY_SECONDS_FULL;
+ *has_restart_replay = false;
+ *restart_replay = false;
+ if(!request->has_data || request->data.type == SJ_NULL)
+ return true;
+
+ if(request->data.type != SJ_OBJECT) {
+ snprintf(error_message, error_message_size, "expected 'data' to be an object with the optional fields 'seconds' and 'restart-replay'");
+ return false;
+ }
+
+ sj_Reader reader = sj_reader(request->data.start, request->request_end - request->data.start);
+ const sj_Value data = sj_read(&reader);
+
+ sj_Value key;
+ sj_Value value;
+ while(sj_iter_object(&reader, data, &key, &value)) {
+ if(gsr_json_string_equals(&key, "seconds")) {
+ if(value.type == SJ_NULL)
+ continue;
+
+ if(!json_value_to_save_replay_seconds(&value, seconds, error_message, error_message_size))
+ return false;
+ } else if(gsr_json_string_equals(&key, "restart-replay")) {
+ if(value.type != SJ_BOOL) {
+ snprintf(error_message, error_message_size, "expected 'restart-replay' to be true or false");
+ return false;
+ }
+
+ *has_restart_replay = true;
+ *restart_replay = gsr_json_string_equals(&value, "true");
+ }
+ }
+
+ if(reader.error) {
+ snprintf(error_message, error_message_size, "failed to parse 'data': %s", reader.error);
+ return false;
+ }
+
+ return true;
+}
+
+static bool ipc_request_get_set_paused_state(const gsr_ipc_request *request, bool *paused, char *error_message, size_t error_message_size) {
+ if(request->has_data && request->data.type == SJ_BOOL) {
+ *paused = gsr_json_string_equals(&request->data, "true");
+ return true;
+ }
+
+ snprintf(error_message, error_message_size, "expected 'data' to be true to pause or false to unpause");
+ return false;
+}
+
+static bool ipc_request_name_to_deferred_request_type(const char *name, gsr_ipc_deferred_request_type *type) {
+ if(strcmp(name, "stop") == 0) {
+ *type = GSR_IPC_DEFERRED_REQUEST_STOP;
+ return true;
+ }
+
+ if(strcmp(name, "save-replay") == 0) {
+ *type = GSR_IPC_DEFERRED_REQUEST_SAVE_REPLAY;
+ return true;
+ }
+
+ if(strcmp(name, "stop-replay-recording") == 0) {
+ *type = GSR_IPC_DEFERRED_REQUEST_STOP_REPLAY_RECORDING;
+ return true;
+ }
+
+ return false;
+}
+
+static const char* deferred_request_already_pending_error(gsr_ipc_deferred_request_type type) {
+ switch(type) {
+ case GSR_IPC_DEFERRED_REQUEST_STOP: return "GPU Screen Recorder is already stopping";
+ case GSR_IPC_DEFERRED_REQUEST_SAVE_REPLAY: return "a replay is already being saved";
+ case GSR_IPC_DEFERRED_REQUEST_STOP_REPLAY_RECORDING: return "the recording is already being stopped";
+ case GSR_IPC_DEFERRED_REQUEST_TYPE_COUNT: break;
+ }
+ return "the request is already being handled";
+}
+
+static const char* deferred_request_failed_error(gsr_ipc_deferred_request_type type) {
+ switch(type) {
+ case GSR_IPC_DEFERRED_REQUEST_STOP: return "failed to save the recording";
+ case GSR_IPC_DEFERRED_REQUEST_SAVE_REPLAY: return "failed to save the replay";
+ case GSR_IPC_DEFERRED_REQUEST_STOP_REPLAY_RECORDING: return "failed to save the recording";
+ case GSR_IPC_DEFERRED_REQUEST_TYPE_COUNT: break;
+ }
+ return "the request failed";
+}
+
+static bool ipc_set_deferred_request_pending(gsr_ipc *self, gsr_ipc_deferred_request_type type, int client_fd, int64_t request_id) {
+ pthread_mutex_lock(&self->deferred_requests_mutex);
+ gsr_ipc_deferred_request *deferred_request = &self->deferred_requests[type];
+ const bool was_empty = deferred_request->state == GSR_IPC_DEFERRED_REQUEST_STATE_EMPTY;
+ if(was_empty) {
+ deferred_request->state = GSR_IPC_DEFERRED_REQUEST_STATE_PENDING;
+ deferred_request->client_fd = client_fd;
+ deferred_request->request_id = request_id;
+ deferred_request->success = false;
+ deferred_request->has_filepath = false;
+ }
+ pthread_mutex_unlock(&self->deferred_requests_mutex);
+ return was_empty;
+}
+
+static void ipc_clear_deferred_request(gsr_ipc *self, gsr_ipc_deferred_request_type type) {
+ pthread_mutex_lock(&self->deferred_requests_mutex);
+ self->deferred_requests[type].state = GSR_IPC_DEFERRED_REQUEST_STATE_EMPTY;
+ pthread_mutex_unlock(&self->deferred_requests_mutex);
+}
+
+static bool ipc_handle_request(gsr_ipc *self, const gsr_ipc_request *request, char *error_message, size_t error_message_size) {
+ if(strcmp(request->name, "stop") == 0)
+ return self->handlers.stop(error_message, error_message_size, self->handlers.userdata);
+
+ if(strcmp(request->name, "toggle-pause") == 0)
+ return self->handlers.toggle_pause(error_message, error_message_size, self->handlers.userdata);
+
+ if(strcmp(request->name, "set-paused") == 0) {
+ bool paused = false;
+ if(!ipc_request_get_set_paused_state(request, &paused, error_message, error_message_size))
+ return false;
+
+ return self->handlers.set_paused(paused, error_message, error_message_size, self->handlers.userdata);
+ }
+
+ if(strcmp(request->name, "toggle-replay-recording") == 0)
+ return self->handlers.toggle_replay_recording(error_message, error_message_size, self->handlers.userdata);
+
+ if(strcmp(request->name, "start-replay-recording") == 0)
+ return self->handlers.start_replay_recording(error_message, error_message_size, self->handlers.userdata);
+
+ if(strcmp(request->name, "stop-replay-recording") == 0)
+ return self->handlers.stop_replay_recording(error_message, error_message_size, self->handlers.userdata);
+
+ if(strcmp(request->name, "save-replay") == 0) {
+ int seconds = GSR_SAVE_REPLAY_SECONDS_FULL;
+ bool has_restart_replay = false;
+ bool restart_replay = false;
+ if(!ipc_request_get_save_replay_options(request, &seconds, &has_restart_replay, &restart_replay, error_message, error_message_size))
+ return false;
+
+ return self->handlers.save_replay(seconds, has_restart_replay, restart_replay, error_message, error_message_size, self->handlers.userdata);
+ }
+
+ snprintf(error_message, error_message_size, "unknown request name '%s'", request->name);
+ return false;
+}
+
+static bool ipc_client_on_request(gsr_ipc *self, gsr_ipc_client *client) {
+ if(client->request_too_large)
+ return ipc_client_send_reply(self, client, 0, false, "the request is too large", NULL);
+
+ if(string_is_only_whitespace(client->request, client->request_size))
+ return ipc_client_send_reply(self, client, 0, false, "the request is empty", NULL);
+
+ char error_message[GSR_IPC_MAX_ERROR_MESSAGE_SIZE];
+ error_message[0] = '\0';
+
+ gsr_ipc_request request;
+ if(!ipc_request_parse(client->request, client->request_size, &request, error_message, sizeof(error_message)))
+ return ipc_client_send_reply(self, client, request.id, false, error_message, NULL);
+
+ /* The pending deferred request has to be registered before the handler starts the operation,
+ otherwise the operation could finish before the reply to it gets registered */
+ gsr_ipc_deferred_request_type deferred_request_type;
+ const bool reply_is_deferred = ipc_request_name_to_deferred_request_type(request.name, &deferred_request_type);
+ if(reply_is_deferred && !ipc_set_deferred_request_pending(self, deferred_request_type, client->fd, request.id))
+ return ipc_client_send_reply(self, client, request.id, false, deferred_request_already_pending_error(deferred_request_type), NULL);
+
+ if(!ipc_handle_request(self, &request, error_message, sizeof(error_message))) {
+ if(reply_is_deferred)
+ ipc_clear_deferred_request(self, deferred_request_type);
+ return ipc_client_send_reply(self, client, request.id, false, error_message, NULL);
+ }
+
+ if(reply_is_deferred)
+ return true;
+
+ return ipc_client_send_reply(self, client, request.id, true, NULL, NULL);
+}
+
+static bool ipc_client_on_byte(gsr_ipc *self, gsr_ipc_client *client, char c) {
+ if(c != '\n') {
+ if(client->request_size < GSR_IPC_MAX_REQUEST_SIZE)
+ client->request[client->request_size++] = c;
+ else
+ client->request_too_large = true;
+ return true;
+ }
+
+ const bool keep_client = ipc_client_on_request(self, client);
+ client->request_size = 0;
+ client->request_too_large = false;
+ return keep_client;
+}
+
+static bool ipc_client_receive(gsr_ipc *self, gsr_ipc_client *client) {
+ for(;;) {
+ char buffer[1024];
+ const ssize_t bytes_read = recv(client->fd, buffer, sizeof(buffer), 0);
+ if(bytes_read == 0)
+ return false;
+
+ if(bytes_read == -1) {
+ if(errno == EINTR)
+ continue;
+
+ return errno == EAGAIN || errno == EWOULDBLOCK;
+ }
+
+ for(ssize_t i = 0; i < bytes_read; ++i) {
+ if(!ipc_client_on_byte(self, client, buffer[i]))
+ return false;
+ }
+ }
+}
+
+static void ipc_add_client(gsr_ipc *self, int client_fd) {
+ if(self->num_clients == GSR_IPC_MAX_CLIENTS) {
+ gsr_log(GSR_LOG_LEVEL_WARNING, "gsr_ipc: too many ipc clients are connected, rejecting the new connection");
+ close(client_fd);
+ return;
+ }
+
+ if(!fd_set_cloexec(client_fd) || !fd_set_nonblocking(client_fd) || !ipc_poller_add(self, client_fd)) {
+ gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_ipc: failed to setup the ipc client socket, error: %s", strerror(errno));
+ close(client_fd);
+ return;
+ }
+
+ gsr_ipc_client *client = &self->clients[self->num_clients];
+ client->fd = client_fd;
+ client->request_size = 0;
+ client->request_too_large = false;
+ client->send_buffer_size = 0;
+ ++self->num_clients;
+}
+
+static void ipc_accept_clients(gsr_ipc *self) {
+ for(;;) {
+ const int client_fd = accept(self->socket_fd, NULL, NULL);
+ if(client_fd == -1) {
+ if(errno == EINTR)
+ continue;
+ return;
+ }
+
+ ipc_add_client(self, client_fd);
+ }
+}
+
+static void ipc_remove_client(gsr_ipc *self, int index) {
+ const int client_fd = self->clients[index].fd;
+
+ pthread_mutex_lock(&self->deferred_requests_mutex);
+ for(int i = 0; i < GSR_IPC_DEFERRED_REQUEST_TYPE_COUNT; ++i) {
+ if(self->deferred_requests[i].state != GSR_IPC_DEFERRED_REQUEST_STATE_EMPTY && self->deferred_requests[i].client_fd == client_fd)
+ self->deferred_requests[i].state = GSR_IPC_DEFERRED_REQUEST_STATE_EMPTY;
+ }
+ pthread_mutex_unlock(&self->deferred_requests_mutex);
+
+ close(client_fd);
+ for(int i = index; i < self->num_clients - 1; ++i) {
+ self->clients[i] = self->clients[i + 1];
+ }
+ --self->num_clients;
+}
+
+static int ipc_find_client_index_by_fd(const gsr_ipc *self, int fd) {
+ for(int i = 0; i < self->num_clients; ++i) {
+ if(self->clients[i].fd == fd)
+ return i;
+ }
+ return -1;
+}
+
+static void ipc_send_completed_request_replies(gsr_ipc *self) {
+ for(int i = 0; i < GSR_IPC_DEFERRED_REQUEST_TYPE_COUNT; ++i) {
+ pthread_mutex_lock(&self->deferred_requests_mutex);
+ const gsr_ipc_deferred_request deferred_request = self->deferred_requests[i];
+ if(deferred_request.state == GSR_IPC_DEFERRED_REQUEST_STATE_COMPLETED)
+ self->deferred_requests[i].state = GSR_IPC_DEFERRED_REQUEST_STATE_EMPTY;
+ pthread_mutex_unlock(&self->deferred_requests_mutex);
+
+ if(deferred_request.state != GSR_IPC_DEFERRED_REQUEST_STATE_COMPLETED)
+ continue;
+
+ const int client_index = ipc_find_client_index_by_fd(self, deferred_request.client_fd);
+ if(client_index == -1)
+ continue;
+
+ const char *error_message = deferred_request_failed_error(i);
+ const char *filepath = deferred_request.has_filepath ? deferred_request.filepath : NULL;
+ if(!ipc_client_send_reply(self, &self->clients[client_index], deferred_request.request_id, deferred_request.success, error_message, filepath))
+ ipc_remove_client(self, client_index);
+ }
+}
+
+static void ipc_fail_pending_requests(gsr_ipc *self) {
+ for(int i = 0; i < GSR_IPC_DEFERRED_REQUEST_TYPE_COUNT; ++i) {
+ pthread_mutex_lock(&self->deferred_requests_mutex);
+ const gsr_ipc_deferred_request deferred_request = self->deferred_requests[i];
+ self->deferred_requests[i].state = GSR_IPC_DEFERRED_REQUEST_STATE_EMPTY;
+ pthread_mutex_unlock(&self->deferred_requests_mutex);
+
+ if(deferred_request.state != GSR_IPC_DEFERRED_REQUEST_STATE_PENDING)
+ continue;
+
+ const int client_index = ipc_find_client_index_by_fd(self, deferred_request.client_fd);
+ if(client_index == -1)
+ continue;
+
+ if(!ipc_client_send_reply(self, &self->clients[client_index], deferred_request.request_id, false, "GPU Screen Recorder exited before the request finished", NULL))
+ ipc_remove_client(self, client_index);
+ }
+}
+
+static void ipc_flush_clients_blocking(gsr_ipc *self) {
+ for(int i = 0; i < self->num_clients; ++i) {
+ gsr_ipc_client *client = &self->clients[i];
+ if(client->send_buffer_size > 0)
+ ipc_send_all_blocking(client->fd, client->send_buffer, client->send_buffer_size);
+ client->send_buffer_size = 0;
+ }
+}
+
+static int ipc_drain_wakeup_pipe(gsr_ipc *self) {
+ int wakeup_flags = 0;
+ for(;;) {
+ char buffer[64];
+ const ssize_t bytes_read = read(self->wakeup_pipe[0], buffer, sizeof(buffer));
+ if(bytes_read == -1 && errno == EINTR)
+ continue;
+
+ if(bytes_read <= 0)
+ break;
+
+ for(ssize_t i = 0; i < bytes_read; ++i) {
+ if(buffer[i] == 'q')
+ wakeup_flags |= GSR_IPC_WAKEUP_QUIT;
+ else if(buffer[i] == 'c')
+ wakeup_flags |= GSR_IPC_WAKEUP_COMPLETED_REQUEST;
+ }
+ }
+ return wakeup_flags;
+}
+
+static void ipc_wakeup_thread(gsr_ipc *self, char wakeup_value) {
+ ssize_t bytes_written = 0;
+ do {
+ bytes_written = write(self->wakeup_pipe[1], &wakeup_value, 1);
+ } while(bytes_written == -1 && errno == EINTR);
+
+ if(bytes_written == -1)
+ gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_ipc: failed to wake up the ipc thread, error: %s", strerror(errno));
+}
+
+static void* ipc_thread(void *userdata) {
+ gsr_ipc *self = userdata;
+ gsr_ipc_event events[GSR_IPC_MAX_EVENTS];
+ bool running = true;
+
+ while(running) {
+ const int num_events = ipc_poller_wait(self, events, GSR_IPC_MAX_EVENTS);
+ if(num_events == -1) {
+ gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_ipc: failed to wait for ipc events, error: %s", strerror(errno));
+ break;
+ }
+
+ for(int i = 0; i < num_events; ++i) {
+ if(events[i].fd == self->wakeup_pipe[0]) {
+ const int wakeup_flags = ipc_drain_wakeup_pipe(self);
+ if(wakeup_flags & GSR_IPC_WAKEUP_COMPLETED_REQUEST)
+ ipc_send_completed_request_replies(self);
+ if(wakeup_flags & GSR_IPC_WAKEUP_QUIT)
+ running = false;
+ continue;
+ }
+
+ if(events[i].fd == self->socket_fd) {
+ ipc_accept_clients(self);
+ continue;
+ }
+
+ const int client_index = ipc_find_client_index_by_fd(self, events[i].fd);
+ if(client_index == -1)
+ continue;
+
+ gsr_ipc_client *client = &self->clients[client_index];
+ bool keep_client = true;
+ if(events[i].readable)
+ keep_client = ipc_client_receive(self, client);
+ if(keep_client && events[i].writable)
+ keep_client = ipc_client_flush_send_buffer(self, client);
+
+ if(!keep_client)
+ ipc_remove_client(self, client_index);
+ }
+ }
+
+ ipc_send_completed_request_replies(self);
+ ipc_fail_pending_requests(self);
+ ipc_flush_clients_blocking(self);
+ return NULL;
+}
+
+static bool ipc_socket_filepath_in_use(const char *socket_filepath) {
+ struct sockaddr_un addr;
+ memset(&addr, 0, sizeof(addr));
+ addr.sun_family = AF_UNIX;
+ snprintf(addr.sun_path, sizeof(addr.sun_path), "%s", socket_filepath);
+
+ const int fd = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0);
+ if(fd == -1)
+ return true;
+
+ const bool in_use = connect(fd, (const struct sockaddr*)&addr, sizeof(addr)) == 0;
+ close(fd);
+ return in_use;
+}
+
+/* Removes the socket that a GPU Screen Recorder instance that was killed left behind, so the same ipc socket path can be used again */
+static bool ipc_remove_unused_socket(const char *socket_filepath) {
+ struct stat file_stat;
+ if(lstat(socket_filepath, &file_stat) == -1)
+ return true;
+
+ if(!S_ISSOCK(file_stat.st_mode)) {
+ gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_ipc_init: can't use \"%s\" as the ipc socket path because a file that isn't a socket already exists there", socket_filepath);
+ return false;
+ }
+
+ if(ipc_socket_filepath_in_use(socket_filepath)) {
+ gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_ipc_init: another program is already listening on \"%s\"", socket_filepath);
+ return false;
+ }
+
+ unlink(socket_filepath);
+ return true;
+}
+
+static bool ipc_bind(gsr_ipc *self, const struct sockaddr_un *addr) {
+ if(!ipc_remove_unused_socket(self->socket_filepath))
+ return false;
+
+ const mode_t prev_mask = umask(0777 & ~GSR_IPC_SOCKET_MODE);
+ const int bind_result = bind(self->socket_fd, (const struct sockaddr*)addr, sizeof(*addr));
+ const int bind_error = errno;
+ umask(prev_mask);
+
+ if(bind_result == -1) {
+ gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_ipc_init: failed to bind the ipc socket to \"%s\", error: %s", self->socket_filepath, strerror(bind_error));
+ return false;
+ }
+
+ self->socket_bound = true;
+ return true;
+}
+
+static void ipc_close(gsr_ipc *self) {
+ for(int i = 0; i < self->num_clients; ++i) {
+ close(self->clients[i].fd);
+ }
+ self->num_clients = 0;
+
+ for(int i = 0; i < 2; ++i) {
+ if(self->wakeup_pipe[i] != -1) {
+ close(self->wakeup_pipe[i]);
+ self->wakeup_pipe[i] = -1;
+ }
+ }
+
+ if(self->poll_fd != -1) {
+ close(self->poll_fd);
+ self->poll_fd = -1;
+ }
+
+ if(self->socket_fd != -1) {
+ close(self->socket_fd);
+ self->socket_fd = -1;
+ }
+
+ if(self->socket_bound) {
+ unlink(self->socket_filepath);
+ self->socket_bound = false;
+ }
+
+ if(self->deferred_requests_mutex_created) {
+ pthread_mutex_destroy(&self->deferred_requests_mutex);
+ self->deferred_requests_mutex_created = false;
+ }
+}
+
+int gsr_ipc_init(gsr_ipc *self, const char *socket_filepath) {
+ memset(self, 0, sizeof(*self));
+ self->socket_fd = -1;
+ self->poll_fd = -1;
+ self->wakeup_pipe[0] = -1;
+ self->wakeup_pipe[1] = -1;
+
+ struct sockaddr_un addr;
+ memset(&addr, 0, sizeof(addr));
+ addr.sun_family = AF_UNIX;
+ if(snprintf(addr.sun_path, sizeof(addr.sun_path), "%s", socket_filepath) >= (int)sizeof(addr.sun_path)) {
+ gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_ipc_init: the ipc socket path is too long, it can be at most %d characters: \"%s\"", (int)sizeof(addr.sun_path) - 1, socket_filepath);
+ goto err;
+ }
+
+ snprintf(self->socket_filepath, sizeof(self->socket_filepath), "%s", socket_filepath);
+
+ if(pthread_mutex_init(&self->deferred_requests_mutex, NULL) != 0) {
+ gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_ipc_init: failed to create the deferred requests mutex");
+ goto err;
+ }
+ self->deferred_requests_mutex_created = true;
+
+ if(pipe(self->wakeup_pipe) == -1) {
+ gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_ipc_init: failed to create the ipc wakeup pipe, error: %s", strerror(errno));
+ self->wakeup_pipe[0] = -1;
+ self->wakeup_pipe[1] = -1;
+ goto err;
+ }
+
+ if(!fd_set_cloexec(self->wakeup_pipe[0]) || !fd_set_cloexec(self->wakeup_pipe[1]) || !fd_set_nonblocking(self->wakeup_pipe[0])) {
+ gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_ipc_init: failed to setup the ipc wakeup pipe, error: %s", strerror(errno));
+ goto err;
+ }
+
+ self->socket_fd = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC | SOCK_NONBLOCK, 0);
+ if(self->socket_fd == -1) {
+ gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_ipc_init: failed to create the ipc socket, error: %s", strerror(errno));
+ goto err;
+ }
+
+ if(!ipc_poller_init(self))
+ goto err;
+
+ if(!ipc_poller_add(self, self->wakeup_pipe[0]) || !ipc_poller_add(self, self->socket_fd)) {
+ gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_ipc_init: failed to register the ipc sockets for events, error: %s", strerror(errno));
+ goto err;
+ }
+
+ if(!ipc_bind(self, &addr))
+ goto err;
+
+ if(listen(self->socket_fd, GSR_IPC_MAX_CLIENTS) == -1) {
+ gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_ipc_init: failed to listen on the ipc socket, error: %s", strerror(errno));
+ goto err;
+ }
+
+ self->initialized = true;
+ return GSR_ERROR_OK;
+
+ err:
+ ipc_close(self);
+ return GSR_ERROR_GENERIC;
+}
+
+void gsr_ipc_deinit(gsr_ipc *self) {
+ if(!self->initialized)
+ return;
+
+ gsr_ipc_stop(self);
+ ipc_close(self);
+ self->initialized = false;
+}
+
+int gsr_ipc_start(gsr_ipc *self, const gsr_ipc_handlers *handlers) {
+ if(!self->initialized)
+ return GSR_ERROR_OK;
+
+ self->handlers = *handlers;
+
+ /* Block all signals in the ipc thread to keep the signal handlers running on the main thread */
+ sigset_t all_signals;
+ sigset_t prev_signals;
+ sigfillset(&all_signals);
+ pthread_sigmask(SIG_SETMASK, &all_signals, &prev_signals);
+ const int thread_create_result = pthread_create(&self->thread, NULL, ipc_thread, self);
+ pthread_sigmask(SIG_SETMASK, &prev_signals, NULL);
+
+ if(thread_create_result != 0) {
+ gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_ipc_start: failed to create the ipc thread, error: %s", strerror(thread_create_result));
+ return GSR_ERROR_GENERIC;
+ }
+
+ self->thread_running = true;
+ return GSR_ERROR_OK;
+}
+
+void gsr_ipc_stop(gsr_ipc *self) {
+ if(!self->thread_running)
+ return;
+
+ ipc_wakeup_thread(self, 'q');
+ pthread_join(self->thread, NULL);
+ self->thread_running = false;
+}
+
+void gsr_ipc_complete_request(gsr_ipc *self, gsr_ipc_deferred_request_type type, bool success, const char *filepath) {
+ if(!self->initialized)
+ return;
+
+ pthread_mutex_lock(&self->deferred_requests_mutex);
+ gsr_ipc_deferred_request *deferred_request = &self->deferred_requests[type];
+ const bool was_pending = deferred_request->state == GSR_IPC_DEFERRED_REQUEST_STATE_PENDING;
+ if(was_pending) {
+ deferred_request->state = GSR_IPC_DEFERRED_REQUEST_STATE_COMPLETED;
+ deferred_request->success = success;
+ deferred_request->has_filepath = filepath != NULL;
+ if(filepath)
+ snprintf(deferred_request->filepath, sizeof(deferred_request->filepath), "%s", filepath);
+ }
+ pthread_mutex_unlock(&self->deferred_requests_mutex);
+
+ if(was_pending)
+ ipc_wakeup_thread(self, 'c');
+}
diff --git a/src/cli/main.c b/src/cli/main.c
new file mode 100644
index 0000000..aaa315c
--- /dev/null
+++ b/src/cli/main.c
@@ -0,0 +1,708 @@
+#include "../../include/cli/commands.h"
+#include "../../include/cli/ipc.h"
+#include "../../include/recorder/recorder.h"
+#include "../../include/recorder/screenshot.h"
+#include "../../include/recorder/capture_source.h"
+#include "../../include/recorder/capture_setup.h"
+#include "../../include/recorder/windowing.h"
+#include "../../include/recorder/audio_input.h"
+#include "../../include/recorder/replay_save.h"
+#include "../../include/recorder/error.h"
+#include "../../include/args_parser.h"
+#include "../../include/sound.h"
+#include "../../include/shader.h"
+#include "../../include/utils.h"
+#include "../../include/log.h"
+#ifdef GSR_APP_AUDIO
+#include "../../include/pipewire_audio.h"
+#endif
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <assert.h>
+#include <locale.h>
+#include <signal.h>
+#include <stdatomic.h>
+#include <unistd.h>
+#include <malloc.h>
+
+static atomic_int running = 1;
+static gsr_recorder *recorder = NULL;
+/* Signals that are received before the recorder has been created are applied when it has been created */
+static volatile sig_atomic_t pending_toggle_pause = 0;
+static volatile sig_atomic_t pending_toggle_replay_recording = 0;
+static volatile sig_atomic_t pending_save_replay_seconds = 0;
+
+static void stop_handler(int signal_value) {
+ (void)signal_value;
+ atomic_store(&running, 0);
+ if(recorder)
+ gsr_recorder_stop(recorder);
+}
+
+static void toggle_pause_handler(int signal_value) {
+ (void)signal_value;
+ if(recorder)
+ gsr_recorder_toggle_pause(recorder);
+ else
+ pending_toggle_pause = 1;
+}
+
+static void toggle_replay_recording_handler(int signal_value) {
+ (void)signal_value;
+ if(recorder)
+ gsr_recorder_toggle_replay_recording(recorder);
+ else
+ pending_toggle_replay_recording = 1;
+}
+
+static void save_replay_seconds_handler(gsr_recorder *rec, int seconds) {
+ if(rec)
+ gsr_recorder_save_replay(rec, seconds, GSR_RESTART_REPLAY_USE_OPTION);
+ else
+ pending_save_replay_seconds = seconds;
+}
+
+static void apply_pending_signals(gsr_recorder *rec) {
+ if(pending_toggle_pause) {
+ pending_toggle_pause = 0;
+ gsr_recorder_toggle_pause(rec);
+ }
+
+ if(pending_toggle_replay_recording) {
+ pending_toggle_replay_recording = 0;
+ gsr_recorder_toggle_replay_recording(rec);
+ }
+
+ if(pending_save_replay_seconds != 0) {
+ const int seconds = pending_save_replay_seconds;
+ pending_save_replay_seconds = 0;
+ gsr_recorder_save_replay(rec, seconds, GSR_RESTART_REPLAY_USE_OPTION);
+ }
+}
+
+static void save_replay_handler(int signal_value) {
+ (void)signal_value;
+ save_replay_seconds_handler(recorder, GSR_SAVE_REPLAY_SECONDS_FULL);
+}
+
+static void save_replay_10_seconds_handler(int signal_value) {
+ (void)signal_value;
+ save_replay_seconds_handler(recorder, 10);
+}
+
+static void save_replay_30_seconds_handler(int signal_value) {
+ (void)signal_value;
+ save_replay_seconds_handler(recorder, 30);
+}
+
+static void save_replay_1_minute_handler(int signal_value) {
+ (void)signal_value;
+ save_replay_seconds_handler(recorder, 60);
+}
+
+static void save_replay_5_minutes_handler(int signal_value) {
+ (void)signal_value;
+ save_replay_seconds_handler(recorder, 60*5);
+}
+
+static void save_replay_10_minutes_handler(int signal_value) {
+ (void)signal_value;
+ save_replay_seconds_handler(recorder, 60*10);
+}
+
+static void save_replay_30_minutes_handler(int signal_value) {
+ (void)signal_value;
+ save_replay_seconds_handler(recorder, 60*30);
+}
+
+static bool ipc_stop_handler(char *error_message, size_t error_message_size, void *userdata) {
+ (void)error_message;
+ (void)error_message_size;
+ (void)userdata;
+ atomic_store(&running, 0);
+ gsr_recorder_stop(recorder);
+ return true;
+}
+
+static bool ipc_toggle_pause_handler(char *error_message, size_t error_message_size, void *userdata) {
+ const gsr_recorder_settings *settings = userdata;
+ if(settings->is_replaying) {
+ snprintf(error_message, error_message_size, "pausing is not supported when recording a replay");
+ return false;
+ }
+
+ gsr_recorder_toggle_pause(recorder);
+ return true;
+}
+
+static bool ipc_set_paused_handler(bool paused, char *error_message, size_t error_message_size, void *userdata) {
+ const gsr_recorder_settings *settings = userdata;
+ if(settings->is_replaying) {
+ snprintf(error_message, error_message_size, "pausing is not supported when recording a replay");
+ return false;
+ }
+
+ gsr_recorder_set_paused(recorder, paused);
+ return true;
+}
+
+static bool ipc_toggle_replay_recording_handler(char *error_message, size_t error_message_size, void *userdata) {
+ const gsr_recorder_settings *settings = userdata;
+ if(!settings->replay_recording_directory) {
+ snprintf(error_message, error_message_size, "option -ro is required to start a recording");
+ return false;
+ }
+
+ gsr_recorder_toggle_replay_recording(recorder);
+ return true;
+}
+
+static bool ipc_start_replay_recording_handler(char *error_message, size_t error_message_size, void *userdata) {
+ const gsr_recorder_settings *settings = userdata;
+ if(!settings->replay_recording_directory) {
+ snprintf(error_message, error_message_size, "option -ro is required to start a recording");
+ return false;
+ }
+
+ gsr_recorder_start_replay_recording(recorder);
+ return true;
+}
+
+static bool ipc_stop_replay_recording_handler(char *error_message, size_t error_message_size, void *userdata) {
+ const gsr_recorder_settings *settings = userdata;
+ if(!settings->replay_recording_directory) {
+ snprintf(error_message, error_message_size, "option -ro is required to start a recording");
+ return false;
+ }
+
+ if(!gsr_recorder_is_replay_recording(recorder)) {
+ snprintf(error_message, error_message_size, "no recording is running");
+ return false;
+ }
+
+ gsr_recorder_stop_replay_recording(recorder);
+ return true;
+}
+
+static bool ipc_save_replay_handler(int seconds, bool has_restart_replay, bool restart_replay, char *error_message, size_t error_message_size, void *userdata) {
+ const gsr_recorder_settings *settings = userdata;
+ if(!settings->is_replaying) {
+ snprintf(error_message, error_message_size, "option -r is required to save a replay");
+ return false;
+ }
+
+ int restart_replay_request = GSR_RESTART_REPLAY_USE_OPTION;
+ if(has_restart_replay)
+ restart_replay_request = restart_replay ? GSR_RESTART_REPLAY_ENABLE : GSR_RESTART_REPLAY_DISABLE;
+
+ gsr_recorder_save_replay(recorder, seconds, restart_replay_request);
+ return true;
+}
+
+static void install_signal_handlers(void) {
+ signal(SIGINT, stop_handler);
+ signal(SIGTERM, stop_handler);
+ signal(SIGUSR1, save_replay_handler);
+ signal(SIGUSR2, toggle_pause_handler);
+ signal(SIGRTMIN, toggle_replay_recording_handler);
+ signal(SIGRTMIN+1, save_replay_10_seconds_handler);
+ signal(SIGRTMIN+2, save_replay_30_seconds_handler);
+ signal(SIGRTMIN+3, save_replay_1_minute_handler);
+ signal(SIGRTMIN+4, save_replay_5_minutes_handler);
+ signal(SIGRTMIN+5, save_replay_10_minutes_handler);
+ signal(SIGRTMIN+6, save_replay_30_minutes_handler);
+}
+
+static void set_display_server_environment_variables(void) {
+ /* Some users dont have properly setup environments (no display manager that does systemctl --user import-environment DISPLAY WAYLAND_DISPLAY) */
+ const char *display = getenv("DISPLAY");
+ if(!display) {
+ display = ":0";
+ setenv("DISPLAY", display, true);
+ }
+
+ const char *wayland_display = getenv("WAYLAND_DISPLAY");
+ if(!wayland_display) {
+ wayland_display = "wayland-0";
+ setenv("WAYLAND_DISPLAY", wayland_display, true);
+ }
+}
+
+static void set_environment_variables(void) {
+ set_display_server_environment_variables();
+
+ /* Linux nvidia driver 580.105.08 added the environment variable CUDA_DISABLE_PERF_BOOST to disable the p2 power level issue,
+ where running cuda (which includes nvenc) causes the gpu to be forcefully set to p2 power level which on many nvidia gpus
+ decreases gpu performance in games. On my GTX 1080 it decreased game performance by 10% for absolutely no reason. */
+ setenv("CUDA_DISABLE_PERF_BOOST", "1", true);
+ /* Stop nvidia driver from buffering frames */
+ setenv("__GL_MaxFramesAllowed", "1", true);
+ /* If this is set to 1 then cuGraphicsGLRegisterImage will fail for egl context with error: invalid OpenGL or DirectX context,
+ so we overwrite it */
+ setenv("__GL_THREADED_OPTIMIZATIONS", "0", true);
+ /* Some people set this to nvidia (for nvdec) or vdpau (for nvidia vdpau), which breaks gpu screen recorder since
+ nvidia doesn't support vaapi and nvidia-vaapi-driver doesn't support encoding yet.
+ Let vaapi find the right vaapi driver instead of forcing a specific one. */
+ unsetenv("LIBVA_DRIVER_NAME");
+ /* Some people set this to force all applications to vsync on nvidia, but this makes eglSwapBuffers never return. */
+ unsetenv("__GL_SYNC_TO_VBLANK");
+ /* Same as above, but for amd/intel */
+ unsetenv("vblank_mode");
+}
+
+static void install_cuda_no_stable_perf_limit(void) {
+ if(access("/proc/driver/nvidia/version", F_OK) != 0)
+ return;
+
+ const char *home = getenv("HOME");
+ if(!home) {
+ gsr_log(GSR_LOG_LEVEL_WARNING, "install_cuda_no_stable_perf_limit: $HOME not set");
+ return;
+ }
+
+ char nv_profiles_path[4096];
+ snprintf(nv_profiles_path, sizeof(nv_profiles_path), "%s/.nv/nvidia-application-profiles-rc.d", home);
+
+ if(create_directory_recursive(nv_profiles_path) != 0) {
+ gsr_log(GSR_LOG_LEVEL_WARNING, "install_cuda_no_stable_perf_limit: failed to create directory: %s", nv_profiles_path);
+ return;
+ }
+
+ snprintf(nv_profiles_path, sizeof(nv_profiles_path), "%s/.nv/nvidia-application-profiles-rc.d/10-gsr-cuda-no-stable-perf-limit", home);
+
+ FILE *f = fopen(nv_profiles_path, "wb");
+ if(!f) {
+ gsr_log(GSR_LOG_LEVEL_WARNING, "install_cuda_no_stable_perf_limit: failed to create file: %s", nv_profiles_path);
+ return;
+ }
+
+ const char *profile_data =
+ "{\n"
+ " \"profiles\": [\n"
+ " {\n"
+ " \"name\": \"CudaNoStablePerfLimit\",\n"
+ " \"settings\": [\"0x166c5e\", 0]\n"
+ " }\n"
+ " ],\n"
+ " \"rules\": [\n"
+ " { \"pattern\": \"gpu-screen-recorder\", \"profile\": \"CudaNoStablePerfLimit\" }\n"
+ " ]\n"
+ "}\n";
+
+ fwrite(profile_data, 1, strlen(profile_data), f);
+ fclose(f);
+}
+
+static int validate_args_with_capture_sources(args_parser *arg_parser, const gsr_capture_sources *capture_sources) {
+ const Arg *output_resolution_arg = args_parser_get_arg(arg_parser, "-s");
+ assert(output_resolution_arg);
+
+ const Arg *region_arg = args_parser_get_arg(arg_parser, "-region");
+ assert(region_arg);
+
+ if(gsr_capture_sources_has_type(capture_sources, GSR_CAPTURE_SOURCE_TYPE_FOCUSED_WINDOW) && output_resolution_arg->num_values == 0) {
+ gsr_log(GSR_LOG_LEVEL_ERROR, "option -s is required when using '-w focused' option");
+ args_parser_print_usage();
+ return GSR_ERROR_GENERIC;
+ }
+
+ const bool is_capturing_region = gsr_capture_sources_has_type(capture_sources, GSR_CAPTURE_SOURCE_TYPE_REGION);
+ if(region_arg->num_values == 0) {
+ if(is_capturing_region && !gsr_capture_sources_has_region_set(capture_sources)) {
+ gsr_log(GSR_LOG_LEVEL_ERROR, "option -region is required when '-w region' is used");
+ args_parser_print_usage();
+ return GSR_ERROR_GENERIC;
+ }
+ } else {
+ if(is_capturing_region) {
+ gsr_log(GSR_LOG_LEVEL_WARNING, "option -region is deprecated, use -w with region directly instead, for example: -w %s", region_arg->values[0]);
+ } else {
+ gsr_log(GSR_LOG_LEVEL_ERROR, "option -region can only be used when option '-w region' is used");
+ args_parser_print_usage();
+ return GSR_ERROR_GENERIC;
+ }
+ }
+
+ if(!arg_parser->settings.restore_portal_session && gsr_capture_sources_has_type(capture_sources, GSR_CAPTURE_SOURCE_TYPE_PORTAL))
+ gsr_log(GSR_LOG_LEVEL_INFO, "option '-w portal' was used without '-restore-portal-session yes'. The previous screencast session will be ignored");
+
+ return GSR_ERROR_OK;
+}
+
+static void screenshot_saved_callback(const char *filepath, void *userdata) {
+ const char *recording_saved_script = userdata;
+ if(recording_saved_script)
+ run_recording_saved_script_async(recording_saved_script, filepath, "screenshot");
+}
+
+typedef struct {
+ const char *recording_saved_script;
+ gsr_ipc *ipc;
+} recorder_callbacks_context;
+
+static void replay_saved_callback(const char *filepath, void *userdata) {
+ recorder_callbacks_context *context = userdata;
+ if(!filepath) {
+ printf("gsr error: Failed to save replay\n");
+ fflush(stdout);
+ gsr_ipc_complete_request(context->ipc, GSR_IPC_DEFERRED_REQUEST_SAVE_REPLAY, false, NULL);
+ return;
+ }
+
+ puts(filepath);
+ fflush(stdout);
+ if(context->recording_saved_script)
+ run_recording_saved_script_async(context->recording_saved_script, filepath, "replay");
+
+ gsr_ipc_complete_request(context->ipc, GSR_IPC_DEFERRED_REQUEST_SAVE_REPLAY, true, filepath);
+}
+
+static void recording_started_callback(const char *filepath, void *userdata) {
+ (void)userdata;
+ if(!filepath) {
+ printf("gsr error: Failed to start recording\n");
+ fflush(stdout);
+ }
+}
+
+static void recording_stopped_callback(const char *filepath, void *userdata) {
+ recorder_callbacks_context *context = userdata;
+ if(!filepath) {
+ printf("gsr error: Failed to save recording\n");
+ fflush(stdout);
+ gsr_ipc_complete_request(context->ipc, GSR_IPC_DEFERRED_REQUEST_STOP_REPLAY_RECORDING, false, NULL);
+ return;
+ }
+
+ puts(filepath);
+ fflush(stdout);
+ if(context->recording_saved_script)
+ run_recording_saved_script_async(context->recording_saved_script, filepath, "regular");
+
+ gsr_ipc_complete_request(context->ipc, GSR_IPC_DEFERRED_REQUEST_STOP_REPLAY_RECORDING, true, filepath);
+}
+
+#ifdef GSR_APP_AUDIO
+static gsr_pipewire_audio pipewire_audio;
+
+static bool app_audio_name_callback(const char *app_name, void *userdata) {
+ gsr_app_audio_names *app_audio_names = userdata;
+ gsr_app_audio_names_add(app_audio_names, app_name);
+ return true;
+}
+
+static int setup_app_audio(gsr_app_audio_names *app_audio_names) {
+ if(!pulseaudio_server_is_pipewire()) {
+ gsr_log(GSR_LOG_LEVEL_ERROR, "your sound server is not PipeWire. Application audio is only available when running PipeWire audio server");
+ return GSR_ERROR_UNSUPPORTED;
+ }
+
+ if(!gsr_pipewire_audio_init(&pipewire_audio)) {
+ gsr_log(GSR_LOG_LEVEL_ERROR, "failed to setup PipeWire audio for application audio capture");
+ return GSR_ERROR_UNSUPPORTED;
+ }
+
+ gsr_pipewire_audio_for_each_app(&pipewire_audio, app_audio_name_callback, app_audio_names);
+ return GSR_ERROR_OK;
+}
+#endif
+
+static int parse_audio_inputs(args_parser *arg_parser, gsr_audio_input_tracks *audio_input_tracks) {
+ const Arg *audio_input_arg = args_parser_get_arg(arg_parser, "-a");
+ assert(audio_input_arg);
+
+ gsr_audio_devices audio_devices;
+ memset(&audio_devices, 0, sizeof(audio_devices));
+ if(audio_input_arg->num_values > 0)
+ get_pulseaudio_inputs(&audio_devices);
+
+ const int parse_result = gsr_audio_input_tracks_parse(audio_input_tracks, audio_input_arg->values, audio_input_arg->num_values, &audio_devices);
+ gsr_audio_devices_deinit(&audio_devices);
+ return parse_result;
+}
+
+static int take_screenshot(args_parser *arg_parser, gsr_windowing *windowing, gsr_capture_deps *capture_deps, gsr_capture_sources *capture_sources, gsr_image_format image_format) {
+ const Arg *plugin_arg = args_parser_get_arg(arg_parser, "-p");
+ assert(plugin_arg);
+
+ arg_parser->settings.fps = 60; /* We want to capture an image as soon as possible */
+
+ gsr_screenshot_params screenshot_params;
+ memset(&screenshot_params, 0, sizeof(screenshot_params));
+ screenshot_params.settings = &arg_parser->settings;
+ screenshot_params.egl = &windowing->egl;
+ screenshot_params.window = windowing->window;
+ screenshot_params.capture_deps = capture_deps;
+ screenshot_params.capture_sources = capture_sources;
+ screenshot_params.image_format = image_format;
+ screenshot_params.plugin_filepaths = plugin_arg->values;
+ screenshot_params.num_plugin_filepaths = plugin_arg->num_values;
+ screenshot_params.running = &running;
+ screenshot_params.screenshot_saved = screenshot_saved_callback;
+ screenshot_params.userdata = (void*)arg_parser->settings.recording_saved_script;
+
+ return gsr_screenshot_take(&screenshot_params);
+}
+
+static int record(args_parser *arg_parser, gsr_windowing *windowing, gsr_capture_deps *capture_deps, gsr_capture_sources *capture_sources, gsr_audio_input_tracks *audio_input_tracks, gsr_ipc *ipc) {
+ const Arg *plugin_arg = args_parser_get_arg(arg_parser, "-p");
+ assert(plugin_arg);
+
+ gsr_recorder_params recorder_params;
+ memset(&recorder_params, 0, sizeof(recorder_params));
+ recorder_params.settings = &arg_parser->settings;
+ recorder_params.windowing = windowing;
+ recorder_params.capture_deps = capture_deps;
+ recorder_params.capture_sources = capture_sources;
+ recorder_params.audio_input_tracks = audio_input_tracks;
+ recorder_params.plugin_filepaths = plugin_arg->values;
+ recorder_params.num_plugin_filepaths = plugin_arg->num_values;
+#ifdef GSR_APP_AUDIO
+ recorder_params.pipewire_audio = &pipewire_audio;
+#endif
+
+ recorder_callbacks_context callbacks_context;
+ callbacks_context.recording_saved_script = arg_parser->settings.recording_saved_script;
+ callbacks_context.ipc = ipc;
+
+ gsr_recorder_callbacks callbacks;
+ memset(&callbacks, 0, sizeof(callbacks));
+ callbacks.replay_saved = replay_saved_callback;
+ callbacks.recording_started = recording_started_callback;
+ callbacks.recording_stopped = recording_stopped_callback;
+ callbacks.userdata = &callbacks_context;
+
+ int error = GSR_ERROR_OK;
+ recorder = gsr_recorder_create(&recorder_params, &callbacks, &error);
+ if(!recorder)
+ return error;
+
+ apply_pending_signals(recorder);
+ if(!atomic_load(&running))
+ gsr_recorder_stop(recorder);
+
+ gsr_ipc_handlers ipc_handlers;
+ memset(&ipc_handlers, 0, sizeof(ipc_handlers));
+ ipc_handlers.stop = ipc_stop_handler;
+ ipc_handlers.toggle_pause = ipc_toggle_pause_handler;
+ ipc_handlers.set_paused = ipc_set_paused_handler;
+ ipc_handlers.toggle_replay_recording = ipc_toggle_replay_recording_handler;
+ ipc_handlers.start_replay_recording = ipc_start_replay_recording_handler;
+ ipc_handlers.stop_replay_recording = ipc_stop_replay_recording_handler;
+ ipc_handlers.save_replay = ipc_save_replay_handler;
+ ipc_handlers.userdata = &arg_parser->settings;
+
+ int run_result = gsr_ipc_start(ipc, &ipc_handlers);
+ if(run_result == GSR_ERROR_OK)
+ run_result = gsr_recorder_run(recorder);
+
+ gsr_ipc_complete_request(ipc, GSR_IPC_DEFERRED_REQUEST_STOP, true, arg_parser->settings.is_replaying ? NULL : arg_parser->settings.filename);
+ gsr_ipc_stop(ipc);
+ gsr_recorder_destroy(recorder, true);
+ recorder = NULL;
+ return run_result;
+}
+
+static int run(args_parser *arg_parser) {
+ int exit_code = 0;
+
+ gsr_capture_sources capture_sources;
+ gsr_audio_input_tracks audio_input_tracks;
+ gsr_app_audio_names app_audio_names;
+ gsr_windowing windowing;
+ gsr_capture_deps capture_deps;
+ gsr_ipc ipc;
+ memset(&audio_input_tracks, 0, sizeof(audio_input_tracks));
+ memset(&app_audio_names, 0, sizeof(app_audio_names));
+ memset(&windowing, 0, sizeof(windowing));
+ memset(&ipc, 0, sizeof(ipc));
+ gsr_capture_deps_init(&capture_deps);
+
+ const Arg *ipc_arg = args_parser_get_arg(arg_parser, "-ipc");
+ assert(ipc_arg);
+
+ const int parse_capture_sources_result = gsr_capture_sources_parse(&capture_sources, arg_parser->settings.capture_source, arg_parser->settings.region_position, arg_parser->settings.region_size);
+ if(parse_capture_sources_result != GSR_ERROR_OK) {
+ exit_code = gsr_error_to_exit_code(parse_capture_sources_result);
+ goto done;
+ }
+
+ if(capture_sources.num_items == 0) {
+ gsr_log(GSR_LOG_LEVEL_ERROR, "option -w can't be empty. You need to capture video from at least one source");
+ args_parser_print_usage();
+ exit_code = 1;
+ goto done;
+ }
+
+ const int validate_args_result = validate_args_with_capture_sources(arg_parser, &capture_sources);
+ if(validate_args_result != GSR_ERROR_OK) {
+ exit_code = gsr_error_to_exit_code(validate_args_result);
+ goto done;
+ }
+
+ if(ipc_arg->num_values > 0 && gsr_ipc_init(&ipc, ipc_arg->values[0]) != GSR_ERROR_OK) {
+ exit_code = 1;
+ goto done;
+ }
+
+ const int parse_audio_inputs_result = parse_audio_inputs(arg_parser, &audio_input_tracks);
+ if(parse_audio_inputs_result != GSR_ERROR_OK) {
+ exit_code = gsr_error_to_exit_code(parse_audio_inputs_result);
+ goto done;
+ }
+
+ const bool uses_app_audio = gsr_audio_input_tracks_has_app_audio(&audio_input_tracks);
+#ifdef GSR_APP_AUDIO
+ if(uses_app_audio) {
+ const int app_audio_result = setup_app_audio(&app_audio_names);
+ if(app_audio_result != GSR_ERROR_OK) {
+ exit_code = gsr_error_to_exit_code(app_audio_result);
+ goto done;
+ }
+ }
+#else
+ if(uses_app_audio) {
+ gsr_log(GSR_LOG_LEVEL_ERROR, "application audio can't be recorded because GPU Screen Recorder is built without application audio support (-Dapp_audio option)");
+ exit_code = 2;
+ goto done;
+ }
+#endif
+
+ const int validate_app_audio_result = gsr_audio_input_tracks_validate_app_audio(&audio_input_tracks, &app_audio_names);
+ if(validate_app_audio_result != GSR_ERROR_OK) {
+ exit_code = gsr_error_to_exit_code(validate_app_audio_result);
+ goto done;
+ }
+
+ gsr_windowing_params windowing_params;
+ windowing_params.monitor_capture = gsr_capture_sources_has_monitor_or_region(&capture_sources);
+ windowing_params.gl_debug = arg_parser->settings.gl_debug;
+ windowing_params.listen_to_x11_events = true;
+ if(gsr_windowing_init(&windowing, &windowing_params) != GSR_ERROR_OK) {
+ exit_code = 1;
+ goto done;
+ }
+
+ if(gsr_capture_sources_has_type(&capture_sources, GSR_CAPTURE_SOURCE_TYPE_PORTAL)) {
+ if(gsr_windowing_is_using_prime_run()) {
+ gsr_log(GSR_LOG_LEVEL_WARNING, "use of prime-run with -w portal option is currently not supported. Disabling prime-run");
+ gsr_windowing_disable_prime_run();
+ }
+
+ if(video_codec_is_hdr(arg_parser->settings.video_codec)) {
+ gsr_log(GSR_LOG_LEVEL_WARNING, "portal capture option doesn't support hdr yet (PipeWire doesn't support hdr), the video will be tonemapped from hdr to sdr");
+ arg_parser->settings.video_codec = hdr_video_codec_to_sdr_video_codec(arg_parser->settings.video_codec);
+ }
+ }
+
+ if(gsr_windowing_load_egl(&windowing, &windowing_params) != GSR_ERROR_OK) {
+ exit_code = 1;
+ goto done;
+ }
+
+ gsr_shader_enable_debug_output(arg_parser->settings.gl_debug);
+#ifndef NDEBUG
+ gsr_shader_enable_debug_output(true);
+#endif
+
+ if(!args_parser_validate_with_gl_info(arg_parser, &windowing.egl)) {
+ exit_code = 1;
+ goto done;
+ }
+
+ if(!windowing.card_path_found) {
+ gsr_log(GSR_LOG_LEVEL_ERROR, "no /dev/dri/cardX device found. Make sure that you have at least one monitor connected or record a single window instead on X11 or record with the -w portal option");
+ exit_code = 2;
+ goto done;
+ }
+
+ gsr_capture_deps_init_cursor(&capture_deps, &windowing.egl, arg_parser->settings.record_cursor);
+
+ gsr_image_format image_format;
+ if(get_image_format_from_filename(arg_parser->settings.filename, &image_format)) {
+ if(audio_input_tracks.num_items > 0) {
+ gsr_log(GSR_LOG_LEVEL_ERROR, "can't record audio (-a) when taking a screenshot");
+ exit_code = 1;
+ goto done;
+ }
+
+ if(ipc_arg->num_values > 0)
+ gsr_log(GSR_LOG_LEVEL_WARNING, "option -ipc has no effect when taking a screenshot");
+
+ exit_code = gsr_error_to_exit_code(take_screenshot(arg_parser, &windowing, &capture_deps, &capture_sources, image_format));
+ } else {
+ exit_code = gsr_error_to_exit_code(record(arg_parser, &windowing, &capture_deps, &capture_sources, &audio_input_tracks, &ipc));
+ }
+
+ done:
+ gsr_ipc_deinit(&ipc);
+ gsr_capture_deps_deinit(&capture_deps);
+ gsr_windowing_deinit(&windowing);
+#ifdef GSR_APP_AUDIO
+ gsr_pipewire_audio_deinit(&pipewire_audio);
+#endif
+ gsr_app_audio_names_deinit(&app_audio_names);
+ gsr_audio_input_tracks_deinit(&audio_input_tracks);
+ gsr_capture_sources_deinit(&capture_sources);
+ return exit_code;
+}
+
+int main(int argc, char **argv) {
+ setlocale(LC_ALL, "C"); /* Sigh... stupid C */
+#ifdef __GLIBC__
+ mallopt(M_MMAP_THRESHOLD, 65536);
+#endif
+
+ install_signal_handlers();
+ set_environment_variables();
+ install_cuda_no_stable_perf_limit();
+
+ if(geteuid() == 0) {
+ gsr_log(GSR_LOG_LEVEL_ERROR, "don't run gpu-screen-recorder as the root user");
+ _exit(1);
+ }
+
+ args_handlers arg_handlers;
+ arg_handlers.version = version_command;
+ arg_handlers.info = info_command;
+ arg_handlers.list_audio_devices = list_audio_devices_command;
+ arg_handlers.list_application_audio = list_application_audio_command;
+ arg_handlers.list_v4l2_devices = list_v4l2_devices;
+ arg_handlers.list_capture_options = list_capture_options_command;
+ arg_handlers.list_monitors = list_monitors_command;
+
+ args_parser arg_parser;
+ int exit_code = 0;
+ int command_exit_code = 0;
+ switch(args_parser_parse(&arg_parser, argc, argv, &arg_handlers, NULL, &command_exit_code)) {
+ case ARGS_PARSE_RESULT_ERROR:
+ exit_code = 1;
+ break;
+ case ARGS_PARSE_RESULT_COMMAND_HANDLED:
+ exit_code = command_exit_code;
+ break;
+ case ARGS_PARSE_RESULT_OK: {
+ if(!arg_parser.settings.low_power) {
+ /* Forces low latency encoding mode. Use this environment variable until vaapi supports setting this as a parameter.
+ The downside of this is that it always uses maximum power, which is not ideal for replay mode that runs on system startup.
+ This option was added in mesa 24.1.4, released in july 17, 2024.
+ Seems like the performance issue is not in encoding, but rendering the frame.
+ Some frames end up taking 10 times longer. Seems to be an issue with amd gpu power management when letting the application sleep on the cpu side? */
+ setenv("AMD_DEBUG", "lowlatencyenc", true);
+ }
+
+ exit_code = run(&arg_parser);
+ break;
+ }
+ }
+
+ args_parser_deinit(&arg_parser);
+
+ /* We do an _exit here because cuda uses at_exit to do _something_ that causes the program to freeze,
+ but only on some nvidia driver versions on some gpus (RTX?), and _exit exits the program without calling
+ the at_exit registered functions.
+ Cuda (nvenc) is loaded in a separate process, but this still happens. */
+ _exit(exit_code);
+}
diff --git a/src/codec_query/nvenc.c b/src/codec_query/nvenc.c
index 37c25ba..4416b91 100644
--- a/src/codec_query/nvenc.c
+++ b/src/codec_query/nvenc.c
@@ -1,9 +1,9 @@
#include "../../include/codec_query/nvenc.h"
+#include "../../include/log.h"
#include "../../include/cuda.h"
#include "../../external/nvEncodeAPI.h"
#include <dlfcn.h>
-#include <stdio.h>
#include <string.h>
#define NVENCAPI_MAJOR_VERSION_470 11
@@ -17,7 +17,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 +107,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 +116,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 +157,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 +166,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 +198,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 +209,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 +217,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 +232,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(&params, &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..ca4b3f4 100644
--- a/src/codec_query/vaapi.c
+++ b/src/codec_query/vaapi.c
@@ -1,8 +1,8 @@
#include "../../include/codec_query/vaapi.h"
+#include "../../include/log.h"
#include "../../include/utils.h"
#include <stdlib.h>
-#include <stdio.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
@@ -134,7 +134,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 +207,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 <stdio.h>
@@ -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 <stdio.h>
#include <string.h>
@@ -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..54dac63 100644
--- a/src/cuda.c
+++ b/src/cuda.c
@@ -1,9 +1,8 @@
#include "../include/cuda.h"
+#include "../include/log.h"
#include "../include/library_loader.h"
#include <string.h>
-#include <stdio.h>
#include <dlfcn.h>
-#include <assert.h>
bool gsr_cuda_load(gsr_cuda *self) {
memset(self, 0, sizeof(gsr_cuda));
@@ -13,7 +12,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 +44,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 +52,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 +69,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 +77,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..e8595aa 100644
--- a/src/cursor.c
+++ b/src/cursor.c
@@ -1,6 +1,6 @@
#include "../include/cursor.h"
+#include "../include/log.h"
-#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
@@ -81,7 +81,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..cc122cc 100644
--- a/src/damage.c
+++ b/src/damage.c
@@ -1,8 +1,8 @@
#include "../include/damage.h"
+#include "../include/log.h"
#include "../include/utils.h"
#include "../include/window/window.h"
-#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <X11/extensions/Xdamage.h>
@@ -46,13 +46,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 +71,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 +98,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 +163,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 +173,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 +250,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 <sys/random.h>
@@ -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..933a997 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"
@@ -8,7 +9,6 @@
#include <stdlib.h>
#include <dlfcn.h>
#include <assert.h>
-#include <unistd.h>
// TODO: rename gsr_egl to something else since this includes both egl and glx and in the future maybe vulkan too
@@ -53,34 +53,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 +141,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 +149,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 +193,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 +221,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 +251,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 +343,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 +358,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 +378,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 +407,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 +415,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..e9b71d7 100644
--- a/src/encoder/encoder.c
+++ b/src/encoder/encoder.c
@@ -1,5 +1,7 @@
#include "../../include/encoder/encoder.h"
+#include "../../include/log.h"
#include "../../include/utils.h"
+#include "../../include/ffmpeg_utils.h"
#include <string.h>
#include <stdio.h>
@@ -25,7 +27,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,15 +43,15 @@ 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_encoder_deinit(self);
+ gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_encoder_init: failed to create mutex");
+ gsr_encoder_deinit(self, false);
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_encoder_deinit(self);
+ gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_encoder_init: failed to create mutex");
+ gsr_encoder_deinit(self, false);
return false;
}
self->replay_mutex_created = true;
@@ -57,8 +59,8 @@ 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_encoder_deinit(self);
+ gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_encoder_init: failed to create replay buffer");
+ gsr_encoder_deinit(self, false);
return false;
}
}
@@ -66,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) {
@@ -79,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);
}
@@ -120,7 +125,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);
}
@@ -151,11 +156,13 @@ 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)
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 +174,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 +186,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..594416c 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;
}
@@ -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/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..12dd3d7 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)
@@ -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 3b7c567..232a37d 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"
@@ -36,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;
@@ -73,12 +75,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)
@@ -100,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)
@@ -118,13 +121,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 +140,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;
}
@@ -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++) {
@@ -230,7 +242,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 +252,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 +275,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 +297,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 +305,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 +339,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 +350,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 +382,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 +393,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,11 +401,26 @@ 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;
}
- 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 = {
@@ -484,7 +511,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)
@@ -835,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/ffmpeg_utils.c b/src/ffmpeg_utils.c
new file mode 100644
index 0000000..fc4e4ed
--- /dev/null
+++ b/src/ffmpeg_utils.c
@@ -0,0 +1,40 @@
+#include "../include/ffmpeg_utils.h"
+#include "../include/log.h"
+
+#include <string.h>
+#include <libavutil/error.h>
+#include <libavutil/opt.h>
+#include <libavformat/avformat.h>
+
+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;
+}
+
+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/image_writer.c b/src/image_writer.c
index adc88a7..117c8b8 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"
@@ -8,7 +9,6 @@
#include <stdlib.h>
#include <stdint.h>
#include <stdio.h>
-#include <assert.h>
#include <dlfcn.h>
#define TJPF_RGBA 7
@@ -83,7 +83,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 +128,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 +136,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/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 <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <errno.h>
+
+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';
+ }
+}
diff --git a/src/kde_night_light.c b/src/kde_night_light.c
index d5c9578..d3491ef 100644
--- a/src/kde_night_light.c
+++ b/src/kde_night_light.c
@@ -1,8 +1,8 @@
#include "../include/kde_night_light.h"
+#include "../include/log.h"
#ifdef GSR_DBUS
-#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
@@ -342,11 +342,10 @@ bool gsr_kde_night_light_get_inverse_matrix(gsr_kde_night_light *self, gsr_night
#else /* GSR_DBUS */
-#include <stdio.h>
#include <stddef.h>
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..8b2e8a1 100644
--- a/src/library_loader.c
+++ b/src/library_loader.c
@@ -1,8 +1,8 @@
#include "../include/library_loader.h"
+#include "../include/log.h"
#include <dlfcn.h>
#include <stdbool.h>
-#include <stdio.h>
void* dlsym_print_fail(void *handle, const char *name, bool required) {
dlerror();
@@ -10,7 +10,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 <stdio.h>
+#include <stdarg.h>
+
+#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
deleted file mode 100644
index 06996c3..0000000
--- a/src/main.cpp
+++ /dev/null
@@ -1,4734 +0,0 @@
-extern "C" {
-#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 <assert.h>
-#include <stdio.h>
-#include <stdlib.h>
-#include <string>
-#include <thread>
-#include <mutex>
-#include <signal.h>
-#include <sys/stat.h>
-#include <unistd.h>
-#include <sys/wait.h>
-#include <inttypes.h>
-#include <libgen.h>
-#include <malloc.h>
-
-#include "../include/sound.hpp"
-
-extern "C" {
-#include <libavutil/pixfmt.h>
-#include <libavcodec/avcodec.h>
-#include <libavformat/avformat.h>
-#include <libavutil/opt.h>
-#include <libswresample/swresample.h>
-#include <libavutil/avutil.h>
-#include <libavutil/time.h>
-#include <libavutil/mastering_display_metadata.h>
-#include <libavfilter/avfilter.h>
-#include <libavfilter/buffersink.h>
-#include <libavfilter/buffersrc.h>
-}
-
-#include <future>
-
-#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 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;
-
-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;
- }
-}
-
-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;
-}
-
-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) {
- 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");
- 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 AudioFormat 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;
- }
-}
-
-static AVSampleFormat audio_format_to_sample_format(const AudioFormat 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;
- }
- 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) {
- fprintf(stderr, "gsr error: Could not find %s audio encoder\n", 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<int>::max();
- //codec_context->keyint_min = std::numeric_limits<int>::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) {
- fprintf(stderr, "gsr error: Could not open video codec: %s\n", 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) {
- fprintf(stderr, "gsr error: Could not open video codec: %s\n", av_error_to_string(ret));
- _exit(1);
- }
-}
-
-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;
-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 = 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;
-}
-
-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) {
- fprintf(stderr, "gsr error: Could not allocate stream\n");
- _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';
- if(!realpath(script_file, script_file_full)) {
- fprintf(stderr, "gsr error: script file not found: %s\n", 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);
- }
-}
-
-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;
- 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<AudioDeviceData> 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;
- 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;
-
- 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");
- }
-}
-
-struct RecordingStartAudio {
- const AudioTrack *audio_track;
- AVStream *stream;
-};
-
-struct RecordingStartResult {
- AVFormatContext *av_format_context = nullptr;
- AVStream *video_stream = nullptr;
- std::vector<RecordingStartAudio> 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<AudioTrack> &audio_tracks, bool hdr, std::vector<VideoSource> &video_sources) {
- AVFormatContext *av_format_context;
- avformat_alloc_output_context2(&av_format_context, nullptr, arg_parser.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_tracks.size());
-
- 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)
- 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});
- }
-
- 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));
- 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);
-
- 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));
- avio_close(av_format_context->pb);
- avformat_free_context(av_format_context);
- return result;
- }
-
- for(VideoSource &video_source : video_sources) {
- 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<bool> 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) {
- std::string output_filepath;
- 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());
- 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());
- output_filepath = directory + "/" + filename_prefix + "_" + get_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 std::vector<AudioTrack> &audio_tracks, gsr_encoder *encoder, const args_parser &arg_parser, const std::string &file_extension, bool date_folders, bool hdr, std::vector<VideoSource> &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
- fprintf(stderr, "gsr error: failed to save replay: failed to clone replay buffer\n");
- 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");
- 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<AudioPtsOffset> audio_pts_offsets;
- audio_pts_offsets.reserve(audio_tracks.size());
- for(const AudioTrack &audio_track : audio_tracks) {
- 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.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);
- 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) {
- fprintf(stderr, "gsr error: save_replay_async: no replay packet\n");
- success = false;
- break;
- }
-
- if(!replay_packet->data && !replay_packet_data) {
- fprintf(stderr, "gsr error: save_replay_async: no replay packet data\n");
- 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) {
- fprintf(stderr, "gsr error: save_replay_async: failed to find audio stream by index: %d\n", av_packet.stream_index);
- free(replay_packet_data);
- continue;
- }
-
- const AudioTrack *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)
- 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);
-
- 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 split_string(const std::string &str, char delimiter, std::function<bool(const char*,size_t)> 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<AudioDevice> &audio_devices, const char *name) {
- for(const auto &audio_device : audio_devices) {
- if(strcmp(audio_device.name.c_str(), name) == 0)
- return &audio_device;
- }
- return nullptr;
-}
-
-static MergedAudioInputs parse_audio_input_arg(const char *str) {
- MergedAudioInputs result;
- result.track_name = str;
-
- split_string(str, '|', [&](const char *sub, size_t size) {
- if(size == 0)
- return true;
-
- AudioInput audio_input;
- audio_input.name.assign(sub, size);
-
- 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 result;
-}
-
-static int init_filter_graph(AVCodecContext* audio_codec_context, AVFilterGraph** graph, AVFilterContext** sink, std::vector<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 = 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);
- fprintf(stderr, "gsr warning: your ffmpeg version doesn't support disabling normalizing of mixed audio. Volume might be lower than expected\n");
-#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 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);
- gsr_video_encoder *video_encoder = nullptr;
-
- if(arg_parser.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(&params);
- return video_encoder;
- }
-
- if(video_codec_is_vulkan(arg_parser.video_codec)) {
- gsr_video_encoder_vulkan_params params;
- params.egl = egl;
- params.color_depth = color_depth;
- video_encoder = gsr_video_encoder_vulkan_create(&params);
- 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(&params);
- 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(&params);
- 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)
- *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;
-#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 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;
- 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");
-}
-
-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;
-} 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);
-}
-
-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;
- fprintf(stderr, "gsr warning: failed to connect to the X server. Assuming wayland is running without Xwayland\n");
- }
-
- 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.
- fprintf(stderr, "gsr warning: use of prime-run on X11 is not supported. Disabling prime-run\n");
- disable_prime_run();
- }
-
- setup.window = gsr_window_create(setup.dpy, wayland);
- if(!setup.window) {
- fprintf(stderr, "gsr error: failed to create window\n");
- _exit(1);
- }
-
- setup.list_monitors = true;
-
- if(setup_egl) {
- if(!gsr_egl_load(&setup.egl, setup.window, false, false)) {
- fprintf(stderr, "gsr error: failed to load opengl\n");
- _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)) {
- fprintf(stderr, "gsr error: no /dev/dri/cardX device found. Make sure that you have at least one monitor connected\n");
- 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;
-
- av_log_set_level(AV_LOG_FATAL);
-
- puts("section=system_info");
- list_system_info(wayland);
- if(windowing_setup.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);
- puts("section=video_codecs");
- list_supported_video_codecs(&windowing_setup.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);
-
- 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;
- const AudioDevices audio_devices = get_pulseaudio_inputs();
-
- if(!audio_devices.default_output.empty())
- puts("default_output|Default output");
-
- if(!audio_devices.default_input.empty())
- 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());
- }
-
- 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;
- WindowingSetup windowing_setup = setup_windowing(card_path != nullptr);
-
- if(card_path)
- list_supported_capture_options(windowing_setup.window, card_path, true);
- else
- list_supported_capture_options(windowing_setup.window, windowing_setup.egl.card_path, windowing_setup.list_monitors);
-
- 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;
- WindowingSetup windowing_setup = setup_windowing(true);
-
- if(windowing_setup.list_monitors)
- list_monitors(windowing_setup.window, windowing_setup.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 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 {
- fprintf(stderr, "gsr error: no usable output found\n");
- _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());
- 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.record_cursor;
- ximage_params.output_resolution = arg_parser.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.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.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";
- 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_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.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.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;
- 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);
-
- 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) {
- 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");
- _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);
- 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) {
- fprintf(stderr, "gsr error: desktop portal capture is not supported on X11\n");
- _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.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;
- capture = gsr_capture_portal_create(&portal_params);
- 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");
- _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.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) {
- 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");
- _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.record_cursor;
- xcomposite_params.output_resolution = arg_parser.output_resolution;
- capture = gsr_capture_xcomposite_create(&xcomposite_params);
- if(!capture)
- _exit(1);
- }
-
- return capture;
-}
-
-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 bool any_video_sources_uses_external_image(std::vector<VideoSource> &video_sources) {
- for(VideoSource &video_source : video_sources) {
- if(gsr_capture_uses_external_image(video_source.capture))
- return true;
- }
- return false;
-}
-
-static std::vector<VideoSource> create_video_sources(const args_parser &arg_parser, gsr_egl *egl, bool prefer_ximage, std::vector<CaptureSource> &capture_sources, vec2i &video_size) {
- std::vector<VideoSource> 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.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) {
- fprintf(stderr, "gsr error: gsr_capture_start failed\n");
- _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<CaptureSource> &capture_sources, std::vector<VideoSource> &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);
-
- if(plugin_arg->num_values > 0) {
- const gsr_color_depth color_depth = video_codec_to_bit_depth(arg_parser.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,
- 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, std::vector<CaptureSource> &capture_sources) {
- const int image_quality = video_quality_to_image_quality_value(arg_parser.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
-
- vec2i video_size = {0, 0};
- std::vector<VideoSource> 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);
-
- 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)) {
- fprintf(stderr, "gsr error: capture_image_to_file: gsr_image_write_gl_init failed\n");
- _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 = any_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) {
- fprintf(stderr, "gsr error: capture_image_to_file: failed to create color conversion\n");
- _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(x11_cursor_display && arg_parser.record_cursor)
- gsr_cursor_on_event(&x11_cursor, gsr_window_get_event_data(window));
-
- for(VideoSource &video_source : video_sources) {
- gsr_capture_on_event(video_source.capture, egl);
- }
- }
-
- if(x11_cursor_display && arg_parser.record_cursor)
- gsr_cursor_tick(&x11_cursor, DefaultRootWindow(x11_cursor_display));
-
- gsr_capture_kms_cleanup_kms_fds();
-
- 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);
- }
-
- should_stop_error = false;
- for(VideoSource &video_source : video_sources) {
- gsr_capture_tick(video_source.capture);
- if(gsr_capture_should_stop(video_source.capture, &should_stop_error)) {
- running = 0;
- break;
- }
- }
-
- for(VideoSource &video_source : video_sources) {
- 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(VideoSource &video_source : video_sources) {
- // 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();
-
- 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.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);
- _exit(1);
- }
-
- if(arg_parser.recording_saved_script)
- run_recording_saved_script_async(arg_parser.recording_saved_script, arg_parser.filename, "screenshot");
- }
-
- gsr_plugins_deinit(&plugins);
- gsr_image_writer_deinit(&image_writer);
- for(VideoSource &video_source : video_sources) {
- gsr_capture_destroy(video_source.capture);
- }
- _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<AudioInput> &requested_audio_inputs, const std::vector<std::string> &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) {
- fprintf(stderr, "gsr warning: no audio application with the name \"%s\" was found, expected one of the following:\n", 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<std::string> custom_name;
- std::vector<std::string> devices;
- std::vector<std::string> 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<MergedAudioInputs> parse_audio_inputs(const AudioDevices &audio_devices, const Arg *audio_input_arg) {
- std::vector<MergedAudioInputs> 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.empty()) {
- fprintf(stderr, "gsr error: -a default_output was specified but no default audio output is specified in the audio server\n");
- _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");
- _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());
- if(audio_device) {
- match = true;
- audio_track_description.devices.push_back(audio_device->description);
- }
- }
-
- 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());
- if(!audio_devices.default_output.empty())
- fprintf(stderr, " default_output (Default output)\n");
- if(!audio_devices.default_input.empty())
- 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());
- }
- _exit(50);
- }
- }
-
- requested_audio_inputs.back().track_name = audio_track_description.to_title();
- }
-
- 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;
-}
-
-template <typename T>
-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);
-
- 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 void parse_capture_source_options(const std::string &capture_source_str, CaptureSource &capture_source) {
- bool is_first_column = true;
-
- split_string(capture_source_str, ';', [&](const char *sub, size_t size) {
- if(size == 0)
- return true;
-
- // First column contains the capture target
- if(is_first_column) {
- 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)) {
- fprintf(stderr, "gsr error: invalid capture target value for option x: \"%.*s\", expected a number\n", (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)) {
- fprintf(stderr, "gsr error: invalid capture target value for option y: \"%.*s\", expected a number\n", (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)) {
- fprintf(stderr, "gsr error: invalid capture target value for option width: \"%.*s\", expected a number\n", (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)) {
- fprintf(stderr, "gsr error: invalid capture target value for option height: \"%.*s\", expected a number\n", (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);
- _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);
- _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);
- _exit(1);
- }
- } else if(string_starts_with(sub, size, "hflip=")) {
- sub += 6;
- 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);
- _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)) {
- fprintf(stderr, "gsr error: invalid bool value for option vflip: \"%.*s\", expected a \"true\" or \"false\"\n", (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)) {
- fprintf(stderr, "gsr error: invalid capture target value for option camera_fps: \"%.*s\", expected a number\n", (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);
- _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);
- _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);
- _exit(1);
- }
-
- return true;
- });
-}
-
-static std::vector<CaptureSource> parse_capture_source_arg(const char *capture_source_arg, const args_parser &arg_parser) {
- std::vector<CaptureSource> requested_capture_sources;
- const bool has_multiple_capture_sources = strchr(capture_source_arg, '|') != nullptr;
-
- split_string(capture_source_arg, '|', [&](const char *sub, size_t size) {
- 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 = 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);
- }
-
- capture_source.name.assign(sub, capture_source_size);
-
- 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());
- args_parser_print_usage();
- _exit(1);
- }
- }
-
- 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};
- }
-
- parse_capture_source_options(std::string(substr_start, size), capture_source);
- requested_capture_sources.push_back(capture_source);
- return true;
- });
-
- return requested_capture_sources;
-}
-
-static bool audio_inputs_has_app_audio(const std::vector<AudioInput> &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<MergedAudioInputs> &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<AudioInput> &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<MergedAudioInputs> &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<MergedAudioInputs> &merged_audio_inputs, const std::vector<std::string> &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) {
- 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");
- _exit(2);
- }
- }
-}
-
-static gsr_audio_codec select_audio_codec_with_fallback(gsr_audio_codec audio_codec, const std::string &file_extension, bool uses_amix) {
- switch(audio_codec) {
- case GSR_AUDIO_CODEC_AAC: {
- 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");
- }
- break;
- }
- case GSR_AUDIO_CODEC_OPUS: {
- 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");
- }
- break;
- }
- case GSR_AUDIO_CODEC_FLAC: {
- // TODO: Also check mpegts?
- 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");
- } 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");
- } 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");
- }
- 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 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);
- 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);
-}
-
-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");
- args_parser->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);
-
- 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");
- 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");
- force_cpu_encoding(args_parser);
- }
- break;
- }
- 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");
- 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");
- 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_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: {
- fprintf(stderr, "gsr warning: selected video codec h264_vulkan is not supported by your hardware, trying h264 instead\n");
- 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");
- print_codec_error(args_parser->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: {
- fprintf(stderr, "gsr warning: selected video codec hevc_vulkan is not supported by your hardware, trying hevc instead\n");
- 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");
- print_codec_error(args_parser->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: {
- fprintf(stderr, "gsr warning: selected video codec av1_vulkan is not supported by your hardware, trying av1 instead\n");
- 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");
- print_codec_error(args_parser->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);
- }
-
- if(!video_codec_f) {
- print_codec_error(args_parser->video_codec);
- _exit(54);
- }
-
- *low_power = video_codec_only_supports_low_power_mode(*supported_video_codecs, args_parser->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)) {
- fprintf(stderr, "gsr info: using h264 encoder because a codec was not specified\n");
- 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,
- 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,
- 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->video_codec, args_parser->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)
- ? &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;
- 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");
- 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");
- 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");
- 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");
- 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->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");
- }
- } 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");
- }
- }
-
- 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);
- 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);
- _exit(53);
- }
-
- return codec;
-}
-
-static std::vector<AudioDeviceData> create_device_audio_inputs(const std::vector<AudioInput> &audio_inputs, AVCodecContext *audio_codec_context, int num_channels, double num_audio_frames_shift, std::vector<AVFilterContext*> &src_filter_ctx, bool use_amix) {
- std::vector<AudioDeviceData> audio_track_audio_devices;
- for(size_t i = 0; i < audio_inputs.size(); ++i) {
- const auto &audio_input = audio_inputs[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.empty()) {
- 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) {
- fprintf(stderr, "gsr error: failed to get \"%s\" audio device\n", audio_input.name.c_str());
- _exit(1);
- }
- }
-
- audio_device.frame = create_audio_frame(audio_codec_context);
- 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 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);
- 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))) {
- fprintf(stderr, "gsr error: failed to generate random string\n");
- _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) {
- fprintf(stderr, "gsr error: failed to setup audio recording to combined sink\n");
- _exit(1);
- }
-
- std::vector<const char*> 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());
- }
-
- bool app_audio_inverted = false;
- std::vector<const char*> 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());
- 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())) {
- fprintf(stderr, "gsr error: failed to add application audio link\n");
- _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");
- _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");
- _exit(1);
- }
- }
-
- return audio_device;
-}
-#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")) {
- *image_format = GSR_IMAGE_FORMAT_JPEG;
- return true;
- } else if(string_ends_with(filename, ".png")) {
- *image_format = GSR_IMAGE_FORMAT_PNG;
- return true;
- } else {
- return false;
- }
-}
-
-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", 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<MergedAudioInputs> &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);
-
- return replay_buffer_size_secs * (fps + audio_fps * audio_inputs.size());
-}
-
-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 bool is_capturing_damage_tracked_target(const std::vector<CaptureSource> &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<CaptureSource> &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<CaptureSource> &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<CaptureSource> &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<CaptureSource> &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) {
- fprintf(stderr, "gsr error: option -s is required when using '-w focused' option\n");
- args_parser_print_usage();
- _exit(1);
- }
-
- 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");
- 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]);
- } else {
- fprintf(stderr, "gsr error: option -region can only be used when option '-w region' is used\n");
- 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");
-}
-
-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) {
- fprintf(stderr, "gsr warning: install_cuda_no_stable_perf_limit: $HOME not set\n");
- 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) {
- fprintf(stderr, "gsr warning: install_cuda_no_stable_perf_limit: failed to create directory: %s\n", 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) {
- fprintf(stderr, "gsr warning: install_cuda_no_stable_perf_limit: failed to create file: %s\n", 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) {
- fprintf(stderr, "gsr error: don't run gpu-screen-recorder as the root user\n");
- _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;
- if(!args_parser_parse(&arg_parser, argc, argv, &arg_handlers, NULL))
- _exit(1);
-
- if(!arg_parser.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);
- }
-
- std::vector<CaptureSource> 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");
- 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);
-
- AudioDevices audio_devices;
- if(audio_input_arg->num_values > 0)
- audio_devices = get_pulseaudio_inputs();
-
- std::vector<MergedAudioInputs> requested_audio_inputs = parse_audio_inputs(audio_devices, audio_input_arg);
-
- const bool uses_app_audio = merged_audio_inputs_has_app_audio(requested_audio_inputs);
- std::vector<std::string> 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()) {
- fprintf(stderr, "gsr error: your sound server is not PipeWire. Application audio is only available when running PipeWire audio server\n");
- _exit(2);
- }
-
- if(!gsr_pipewire_audio_init(&pipewire_audio)) {
- fprintf(stderr, "gsr error: failed to setup PipeWire audio for application audio capture\n");
- _exit(2);
- }
-
- gsr_pipewire_audio_for_each_app(&pipewire_audio, [](const char *app_name, void *userdata) {
- std::vector<std::string> *app_audio_names = (std::vector<std::string>*)userdata;
- app_audio_names->push_back(app_name);
- return true;
- }, &app_audio_names);
- }
-#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");
- _exit(2);
- }
-#endif
-
- 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;
- fprintf(stderr, "gsr warning: failed to connect to the X server. Assuming wayland is running without Xwayland\n");
- }
-
- 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.
- fprintf(stderr, "gsr warning: use of prime-run on X11 is not supported. Disabling prime-run\n");
- disable_prime_run();
- }
-
- gsr_window *window = gsr_window_create(dpy, wayland);
- if(!window) {
- fprintf(stderr, "gsr error: failed to create window\n");
- _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");
- 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");
- 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");
- _exit(1);
- }
-
- gsr_shader_enable_debug_output(arg_parser.gl_debug);
-#ifndef NDEBUG
- gsr_shader_enable_debug_output(true);
-#endif
-
- 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))) {
- 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");
- _exit(2);
- }
- } else {
- gsr_get_valid_card_path(&egl, egl.card_path, false);
- }
-
- 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) {
- x11_cursor_display = (Display*)gsr_window_get_display(egl.window);
- gsr_cursor_init(&x11_cursor, &egl, x11_cursor_display);
- }
-
- // 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.filename, &image_format)) {
- if(audio_input_arg->num_values > 0) {
- fprintf(stderr, "gsr error: can't record audio (-a) when taking a screenshot\n");
- _exit(1);
- }
-
- capture_image_to_file(arg_parser, &egl, window, image_format, capture_sources);
- _exit(0);
- }
-
- 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);
- 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);
- } else {
- fprintf(stderr, "gsr error: Failed to deduce container format from file extension. Use the '-c' option to specify container format\n");
- 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.container_format ? arg_parser.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 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);
-
- vec2i video_size = {0, 0};
- std::vector<VideoSource> 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()) {
- fprintf(stderr, "gsr info: live streaming but no audio track was added. Adding a silent audio track\n");
- MergedAudioInputs mai;
- mai.audio_inputs.push_back({""});
- requested_audio_inputs.push_back(std::move(mai));
- }
-
- AVStream *video_stream = nullptr;
- std::vector<AudioTrack> 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");
- _exit(1);
- }
-
- 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)
- video_stream = create_stream(av_format_context, video_codec_context);
-
- AVFrame *video_frame = av_frame_alloc();
- if(!video_frame) {
- fprintf(stderr, "gsr error: Failed to allocate video frame\n");
- _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.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");
- _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");
- _exit(1);
- }
-
- if(!gsr_video_encoder_start(video_encoder, video_codec_context, video_frame)) {
- fprintf(stderr, "gsr error: failed to start video encoder\n");
- _exit(1);
- }
-
- 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);
-
- 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_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.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);
-
- 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");
- _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.video_encoder == GSR_VIDEO_ENCODER_HW_CPU) {
- open_video_software(video_codec_context, arg_parser);
- } else {
- open_video_hardware(video_codec_context, low_power, egl, arg_parser);
- }
-
- 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";
- gsr_encoder_set_recording_destination_first_frame_ts_filepath(&encoder, video_destination_id, ts_filepath.c_str());
- }
- }
-
- 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);
- AVCodecContext *audio_codec_context = create_audio_codec_context(arg_parser.fps, arg_parser.audio_codec, use_amix, arg_parser.audio_bitrate);
-
- AVStream *audio_stream = nullptr;
- 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");
- }
-
- if(audio_stream && !merged_audio_inputs.track_name.empty() && !arg_parser.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(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;
-
- std::vector<AVFilterContext*> src_filter_ctx;
- 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());
- if(err < 0) {
- fprintf(stderr, "gsr error: failed to create audio filter\n");
- _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.audio_codec, arg_parser.fps);// * ((double)audio_codec_context->frame_size / 1024.0);
- const double num_audio_frames_shift = audio_startup_time_seconds / timeout_sec;
-
- std::vector<AudioDeviceData> audio_track_audio_devices;
- if(audio_inputs_has_app_audio(merged_audio_inputs.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);
- }
-
- 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));
- ++audio_stream_index;
-
- audio_max_frame_size = std::max(audio_max_frame_size, audio_codec_context->frame_size);
- }
-
- //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(ret < 0) {
- fprintf(stderr, "gsr error: Could not open '%s': %s\n", arg_parser.filename, av_error_to_string(ret));
- _exit(1);
- }
- }
-
- if(!arg_parser.is_replaying)
- av_write_header(av_format_context, arg_parser.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;
- std::atomic<double> paused_time_offset(0.0);
- double paused_time_start = 0.0;
- bool replay_recording = false;
- RecordingStartResult replay_recording_start_result;
- std::vector<size_t> 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;
-
- 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) {
- fprintf(stderr, "gsr error: failed to create empty audio\n");
- _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<std::mutex> 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) {
- fprintf(stderr, "gsr error: failed to add audio frame to filter\n");
- }
- } 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<std::mutex> 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) {
- fprintf(stderr, "gsr error: failed to add audio frame to filter\n");
- }
- } 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<std::mutex> 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;
- 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.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(gsr_window_get_display_server(window) == GSR_DISPLAY_SERVER_X11) {
- gsr_damage_init(&damage, &egl, &x11_cursor, arg_parser.record_cursor);
- use_damage_tracking = true;
-
- for(const CaptureSource &capture_source : capture_sources) {
- 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.c_str());
- break;
- default:
- break;
- }
- }
- } 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");
- }
- }
-
- while(running) {
- while(gsr_window_process_event(window)) {
- if(x11_cursor_display && arg_parser.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));
- for(VideoSource &video_source : video_sources) {
- gsr_capture_on_event(video_source.capture, &egl);
- }
- }
-
- if(x11_cursor_display && arg_parser.record_cursor)
- gsr_cursor_tick(&x11_cursor, DefaultRootWindow(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(VideoSource &video_source : video_sources) {
- 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.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.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 = clock_get_monotonic_seconds() - paused_time_offset;
- 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_kms_cleanup_kms_fds();
-
- 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);
- }
-
- bool capture_has_synchronous_task = false;
- for(VideoSource &video_source : video_sources) {
- 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_time_start = clock_get_monotonic_seconds();
- paused = true;
- }
- }
- }
-
- for(VideoSource &video_source : video_sources) {
- 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(VideoSource &video_source : video_sources) {
- gsr_capture_capture(video_source.capture, &video_source.metadata, output_color_conversion);
- }
-
- gsr_capture_kms_cleanup_kms_fds();
-
- 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_time_offset = paused_time_offset + (clock_get_monotonic_seconds() - paused_time_start);
- paused = false;
- }
-
- 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) {
- if(hdr && !hdr_metadata_set && !arg_parser.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;
- for(int i = 0; i < num_frames_to_encode; ++i) {
- if(arg_parser.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 {
- fprintf(stderr, "gsr error: avcodec_send_frame failed, error: %s\n", 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.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;
- }
-
- if(toggle_replay_recording && !arg_parser.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) {
- toggle_replay_recording = 0;
- const bool new_replay_recording_state = !replay_recording;
- if(new_replay_recording_state) {
- std::lock_guard<std::mutex> 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_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) {
- 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(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);
- 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");
- } else {
- printf("gsr error: Failed to start recording\n");
- fflush(stdout);
- }
- } else if(replay_recording_start_result.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");
- 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");
- } else {
- printf("gsr error: Failed to save recording\n");
- 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) {
- printf("gsr error: Failed to save replay\n");
- fflush(stdout);
- } 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(save_replay_seconds != 0 && !save_replay_thread.valid() && arg_parser.is_replaying) {
- int current_save_replay_seconds = save_replay_seconds;
- if(current_save_replay_seconds > 0)
- current_save_replay_seconds += arg_parser.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);
- 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) {
- 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 = clock_get_monotonic_seconds() - paused_time_offset;
- 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.framerate_mode == GSR_FRAMERATE_MODE_CONTENT)
- av_usleep(2.8 * 1000.0); // 2.8 milliseconds
- }
- }
-
- running = 0;
-
- if(save_replay_thread.valid()) {
- save_replay_thread.get();
- if(save_replay_output_filepath.empty()) {
- // TODO: Output failed to save
- } 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");
- }
- }
-
- gsr_plugins_deinit(&plugins);
-
- if(replay_recording_start_result.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");
- 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");
- } else {
- printf("gsr error: Failed to save recording\n");
- fflush(stdout);
- }
- }
-
- 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();
-
- // TODO: Replace this with start_recording_create_steams
- if(!arg_parser.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)) {
- avio_close(av_format_context->pb);
- 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);
- }
-#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);
-
- 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(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);
- 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
- // 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/pipewire_audio.c b/src/pipewire_audio.c
index ec4fde1..8f40695 100644
--- a/src/pipewire_audio.c
+++ b/src/pipewire_audio.c
@@ -1,4 +1,6 @@
#include "../include/pipewire_audio.h"
+#include "../include/log.h"
+#include "../include/utils.h"
#include <pipewire/pipewire.h>
#include <pipewire/extensions/metadata.h>
@@ -388,7 +390,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;
}
@@ -399,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) {
- fprintf(stderr, "gsr error: pipewire_audio: failed to reallocate memory\n");
- 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;
@@ -434,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);
@@ -524,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));
@@ -533,7 +517,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;
}
@@ -591,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);
@@ -614,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);
@@ -701,14 +685,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 +700,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;
}
@@ -813,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;
@@ -890,7 +879,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/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 <monitor-name>', 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 <monitor-name>', 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..5b9fd8c 100644
--- a/src/plugins.c
+++ b/src/plugins.c
@@ -1,6 +1,6 @@
#include "../include/plugins.h"
+#include "../include/log.h"
#include "../include/utils.h"
-#include <stdio.h>
#include <string.h>
#include <dlfcn.h>
#include <assert.h>
@@ -23,7 +23,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 +41,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 +54,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 +68,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 +77,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/recorder/audio_capture.c b/src/recorder/audio_capture.c
new file mode 100644
index 0000000..6275c93
--- /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 <string.h>
+#include <stdlib.h>
+#include <stdio.h>
+#include <math.h>
+
+#include <libavutil/opt.h>
+#include <libavutil/time.h>
+#include <libswresample/swresample.h>
+#include <libavfilter/buffersink.h>
+#include <libavfilter/buffersrc.h>
+
+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 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 */
+ 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(atomic_load(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(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];
+ 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 atomic_int *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/audio_codec.c b/src/recorder/audio_codec.c
new file mode 100644
index 0000000..e8f89e7
--- /dev/null
+++ b/src/recorder/audio_codec.c
@@ -0,0 +1,219 @@
+#include "../../include/recorder/audio_codec.h"
+#include "../../include/ffmpeg_utils.h"
+#include "../../include/log.h"
+
+#include <assert.h>
+#include <math.h>
+
+#include <libavutil/opt.h>
+
+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/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 <string.h>
+#include <strings.h>
+#include <stdlib.h>
+#include <stdio.h>
+
+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;
+}
diff --git a/src/recorder/capture_setup.c b/src/recorder/capture_setup.c
new file mode 100644
index 0000000..9fced21
--- /dev/null
+++ b/src/recorder/capture_setup.c
@@ -0,0 +1,571 @@
+#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 <string.h>
+#include <stdlib.h>
+#include <stdio.h>
+#include <unistd.h>
+
+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;
+}
+
+/* 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)
+ 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;
+ }
+ }
+
+ *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;
+ }
+
+ 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 <string.h>
+#include <stdlib.h>
+#include <stdio.h>
+
+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;
+}
diff --git a/src/recorder/codec_select.c b/src/recorder/codec_select.c
new file mode 100644
index 0000000..5f2bb68
--- /dev/null
+++ b/src/recorder/codec_select.c
@@ -0,0 +1,526 @@
+#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 <libavformat/avformat.h>
+
+#include <string.h>
+
+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(&params);
+ 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(&params);
+ return video_encoder;
+ }
+
+ switch(egl->gpu_info.vendor) {
+ case GSR_GPU_VENDOR_AMD:
+ case GSR_GPU_VENDOR_INTEL:
+ case GSR_GPU_VENDOR_BROADCOM:
+ case GSR_GPU_VENDOR_APPLE: {
+ gsr_video_encoder_vaapi_params params;
+ params.egl = egl;
+ params.color_depth = color_depth;
+ video_encoder = gsr_video_encoder_vaapi_create(&params);
+ 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(&params);
+ 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:
+ 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);
+ }
+
+ 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;
+}
diff --git a/src/recorder/muxer.c b/src/recorder/muxer.c
new file mode 100644
index 0000000..806921d
--- /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 <string.h>
+#include <stdlib.h>
+#include <stdio.h>
+
+#include <libavutil/opt.h>
+#include <libavutil/mastering_display_metadata.h>
+
+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(gsr_av_format_context_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/recorder.c b/src/recorder/recorder.c
new file mode 100644
index 0000000..29d36d8
--- /dev/null
+++ b/src/recorder/recorder.c
@@ -0,0 +1,969 @@
+#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 <string.h>
+#include <stdlib.h>
+#include <stdio.h>
+#include <math.h>
+#include <assert.h>
+#include <limits.h>
+#include <unistd.h>
+#include <stdatomic.h>
+
+#include <libavutil/time.h>
+#include <libavformat/avformat.h>
+
+#include <X11/Xlib.h>
+
+#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;
+ 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;
+
+ 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;
+
+ atomic_int running;
+ atomic_int toggle_pause;
+ atomic_int set_paused_request;
+ 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;
+ 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 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) {
+ 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();
+ return GSR_ERROR_GENERIC;
+ }
+ return GSR_ERROR_GENERIC;
+ }
+
+ 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);
+
+ 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) {
+ return 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)) {
+ 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");
+ 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) {
+ 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);
+ 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");
+ return GSR_ERROR_GENERIC;
+ }
+ 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) {
+ 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");
+ 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");
+ 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");
+ return GSR_ERROR_GENERIC;
+ }
+
+ 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) {
+ 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 = 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);
+
+ 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");
+ return GSR_ERROR_GENERIC;
+ }
+
+ 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)) {
+ return GSR_ERROR_GENERIC;
+ }
+ } else {
+ if(!open_video_hardware(self->video_codec_context, self->low_power, self->egl, &self->settings)) {
+ return GSR_ERROR_GENERIC;
+ }
+ }
+
+ 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 + 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);
+ }
+ }
+
+ return 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");
+ }
+
+ 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);
+
+ if(!open_audio(audio_track->codec_context, self->settings.ffmpeg_audio_opts))
+ return GSR_ERROR_GENERIC;
+
+ if(audio_stream)
+ avcodec_parameters_from_context(audio_stream->codecpar, audio_track->codec_context);
+
+ #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(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;
+ }
+ }
+
+ 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);
+ 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;
+
+ if(gsr_audio_inputs_has_app_audio(merged_audio_inputs)) {
+ assert(!use_amix);
+#ifdef GSR_APP_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
+ }
+
+ 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_track.codec_context->frame_size > self->audio_max_frame_size)
+ self->audio_max_frame_size = audio_track.codec_context->frame_size;
+ }
+
+ 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));
+ 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 = &params->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;
+ atomic_init(&self->running, 1);
+ atomic_init(&self->toggle_pause, 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);
+ 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);
+ 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;
+
+ fail:
+ *error = error_code;
+ gsr_recorder_destroy(self, false);
+ 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);
+ }
+ }
+
+ 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);
+
+ 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)) {
+ atomic_store(&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)
+ ++self->damage_fps_counter;
+
+ return 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;
+ const double elapsed = time_now - self->fps_start_time;
+ if (elapsed >= 1.0) {
+ if(self->settings.verbose) {
+ 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;
+ 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);
+ }
+ }
+ }
+
+ 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) {
+ 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);
+
+ 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;
+ }
+
+ // 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;
+ }
+
+ 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;
+ }
+ }
+
+ self->video_pts_counter += num_missed_frames;
+ }
+}
+
+static void recorder_apply_pause_toggle(gsr_recorder *self) {
+ 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");
+ }
+}
+
+static void recorder_apply_replay_recording_toggle(gsr_recorder *self) {
+ 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;
+ }
+
+ 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;
+
+ if(new_replay_recording_state == self->replay_recording)
+ return;
+
+ 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;
+ }
+
+ 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;
+ 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';
+ }
+}
+
+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(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;
+
+ 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(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);
+ }
+ }
+}
+
+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
+ }
+}
+
+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;
+
+ self->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);
+ 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) {
+ /* The audio threads that did start have to stop before they can be joined */
+ atomic_store(&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;
+ 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)");
+ }
+ }
+
+ while(atomic_load(&self->running)) {
+ recorder_process_events(self);
+ const bool damaged = recorder_tick_video_sources(self);
+ recorder_update_fps_counters(self);
+ 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);
+ return self->should_stop_error ? GSR_ERROR_CAPTURE_FAILED : GSR_ERROR_OK;
+}
+
+static void gsr_recorder_stop_recording(gsr_recorder *self) {
+ atomic_store(&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 && gsr_av_format_context_write_trailer(self->av_format_context) != 0) {
+ //fprintf(stderr, "Failed to write trailer\n");
+ }
+
+ if(!self->settings.is_replaying && self->callbacks.recording_stopped)
+ self->callbacks.recording_stopped(self->settings.filename, self->callbacks.userdata);
+}
+
+void gsr_recorder_destroy(gsr_recorder *self, bool exiting) {
+ 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_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, NULL);
+
+ if(self->encoder_initialized)
+ gsr_encoder_deinit(&self->encoder, exiting);
+
+ gsr_video_sources_deinit(&self->video_sources_data);
+
+ 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);
+}
+
+void gsr_recorder_stop(gsr_recorder *self) {
+ atomic_store(&self->running, 0);
+}
+
+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->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, int restart_replay) {
+ atomic_store(&self->save_replay_restart_replay, restart_replay);
+ atomic_store(&self->save_replay_seconds, seconds);
+}
diff --git a/src/recorder/recording_clock.c b/src/recorder/recording_clock.c
new file mode 100644
index 0000000..5c9aabb
--- /dev/null
+++ b/src/recorder/recording_clock.c
@@ -0,0 +1,60 @@
+#include "../../include/recorder/recording_clock.h"
+#include "../../include/utils.h"
+#include "../../include/log.h"
+
+#include <stdlib.h>
+#include <stdatomic.h>
+
+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);
+}
diff --git a/src/recorder/replay_save.c b/src/recorder/replay_save.c
new file mode 100644
index 0000000..3251616
--- /dev/null
+++ b/src/recorder/replay_save.c
@@ -0,0 +1,209 @@
+#include "../../include/recorder/replay_save.h"
+#include "../../include/ffmpeg_utils.h"
+#include "../../include/log.h"
+
+#include <string.h>
+#include <stdlib.h>
+#include <assert.h>
+
+void gsr_replay_save_init(gsr_replay_save *self) {
+ memset(self, 0, sizeof(*self));
+ atomic_init(&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_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);
+
+ //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);
+ atomic_store(&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;
+ atomic_store(&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 false;
+ }
+
+ 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 || !atomic_load(&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);
+}
diff --git a/src/recorder/screenshot.c b/src/recorder/screenshot.c
new file mode 100644
index 0000000..2e4357d
--- /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 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);
+
+ 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;
+ }
+
+ 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");
+ goto done;
+ }
+
+ 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;
+
+ 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");
+ goto done;
+ }
+
+ 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(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));
+
+ 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(atomic_load(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);
+
+ 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);
+ }
+
+ done:
+ 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;
+ }
+}
diff --git a/src/recorder/video_codec.c b/src/recorder/video_codec.c
new file mode 100644
index 0000000..a21e4ec
--- /dev/null
+++ b/src/recorder/video_codec.c
@@ -0,0 +1,428 @@
+#include "../../include/recorder/video_codec.h"
+#include "../../include/ffmpeg_utils.h"
+#include "../../include/log.h"
+
+#include <assert.h>
+
+#include <libavutil/opt.h>
+
+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<int>::max();
+ //codec_context->keyint_min = std::numeric_limits<int>::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;
+ }
+}
diff --git a/src/recorder/windowing.c b/src/recorder/windowing.c
new file mode 100644
index 0000000..0bd1de6
--- /dev/null
+++ b/src/recorder/windowing.c
@@ -0,0 +1,140 @@
+#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 <string.h>
+#include <stdlib.h>
+
+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;
+}
+
+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/replay_buffer/replay_buffer.c b/src/replay_buffer/replay_buffer.c
index 56549ee..2f7db54 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 <stdlib.h>
-#include <string.h>
-#include <assert.h>
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;
@@ -24,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 ce42d93..b61ccfa 100644
--- a/src/replay_buffer/replay_buffer_disk.c
+++ b/src/replay_buffer/replay_buffer_disk.c
@@ -1,8 +1,8 @@
#include "../../include/replay_buffer/replay_buffer_disk.h"
+#include "../../include/log.h"
#include "../../include/utils.h"
#include <stdlib.h>
-#include <string.h>
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
@@ -25,12 +25,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 +39,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 +135,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 +160,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 +233,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;
}
@@ -393,6 +393,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 c5df328..a148a20 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 <stdlib.h>
-#include <string.h>
#include <assert.h>
#include <libavutil/mem.h>
@@ -71,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);
@@ -91,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;
}
@@ -206,6 +210,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;
diff --git a/src/shader.c b/src/shader.c
index f20ebb2..47e152a 100644
--- a/src/shader.c
+++ b/src/shader.c
@@ -1,6 +1,6 @@
#include "../include/shader.h"
+#include "../include/log.h"
#include "../include/egl.h"
-#include <stdio.h>
#include <assert.h>
static bool print_compile_errors = false;
@@ -12,7 +12,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 +28,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 +59,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 +79,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 +106,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.c
index 4e04d8f..2d5d4c2 100644
--- a/src/sound.cpp
+++ b/src/sound.c
@@ -1,14 +1,12 @@
-#include "../include/sound.hpp"
-extern "C" {
+#include "../include/sound.h"
+#include "../include/log.h"
#include "../include/utils.h"
-}
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
-#include <cmath>
-#include <time.h>
-#include <mutex>
+#include <assert.h>
+#include <pthread.h>
#include <pulse/pulseaudio.h>
#include <pulse/mainloop.h>
@@ -33,13 +31,13 @@ extern "C" {
} \
} while(false);
-enum class DeviceType {
- STANDARD,
- DEFAULT_OUTPUT,
- DEFAULT_INPUT
-};
+typedef enum {
+ DEVICE_TYPE_STANDARD,
+ DEVICE_TYPE_DEFAULT_OUTPUT,
+ DEVICE_TYPE_DEFAULT_INPUT
+} DeviceType;
-struct pa_handle {
+typedef struct {
pa_context *context;
pa_stream *stream;
pa_mainloop *mainloop;
@@ -56,7 +54,8 @@ struct pa_handle {
pa_buffer_attr attr;
pa_sample_spec ss;
- std::mutex reconnect_mutex;
+ pthread_mutex_t reconnect_mutex;
+ bool reconnect_mutex_initialized;
DeviceType device_type;
char stream_name[256];
char node_name[256];
@@ -69,7 +68,7 @@ struct pa_handle {
pa_proplist *proplist;
bool connected;
-};
+} pa_handle;
static void pa_sound_device_free(pa_handle *p) {
assert(p);
@@ -100,17 +99,23 @@ static void pa_sound_device_free(pa_handle *p) {
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*, const pa_server_info *server_info, void *userdata) {
+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;
- std::lock_guard<std::mutex> lock(handle->reconnect_mutex);
+ 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 == DeviceType::DEFAULT_OUTPUT && strcmp(handle->device_name, handle->default_output_device_name) != 0) {
+ 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
@@ -121,13 +126,15 @@ static void subscribe_update_default_devices(pa_context*, const pa_server_info *
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) {
+ 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) {
@@ -140,7 +147,8 @@ static void subscribe_cb(pa_context *c, pa_subscription_event_type_t t, uint32_t
}
}
-static void store_default_devices(pa_context*, const pa_server_info *server_info, void *userdata) {
+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);
@@ -151,7 +159,7 @@ static void store_default_devices(pa_context*, const pa_server_info *server_info
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);
+ pa_operation_state_t state = pa_operation_get_state(pa);
if(state == PA_OPERATION_DONE) {
pa_operation_unref(pa);
break;
@@ -163,19 +171,19 @@ 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;
}
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;
+ 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 = DeviceType::DEFAULT_INPUT;
+ p->device_type = DEVICE_TYPE_DEFAULT_INPUT;
} else {
snprintf(p->device_name, sizeof(p->device_name), "%s", device_name);
- p->device_type = DeviceType::STANDARD;
+ p->device_type = DEVICE_TYPE_STANDARD;
}
return true;
@@ -198,17 +206,26 @@ static pa_handle* pa_sound_device_new(const char *server,
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 = DeviceType::STANDARD;
+ p->device_type = DEVICE_TYPE_STANDARD;
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;
+ pa_sound_device_free(p);
return NULL;
}
@@ -286,12 +303,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");
+ if(pa_context_connect(p->context, NULL, PA_CONTEXT_NOFLAGS, NULL) < 0) {
+ gsr_log(GSR_LOG_LEVEL_ERROR, "pa_context_connect failed");
goto fail;
}
@@ -309,7 +326,8 @@ static bool pa_sound_device_handle_context_recreate(pa_handle *p) {
}
static bool pa_sound_device_should_reconnect(pa_handle *p, double now, char *device_name, size_t device_name_size) {
- std::lock_guard<std::mutex> lock(p->reconnect_mutex);
+ 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;
@@ -320,10 +338,11 @@ static bool pa_sound_device_should_reconnect(pa_handle *p, double now, char *dev
p->reconnect_last_tried_seconds = now;
// TODO: Size check
snprintf(device_name, device_name_size, "%s", p->device_name);
- return true;
+ should_reconnect = true;
}
- return false;
+ 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) {
@@ -362,8 +381,9 @@ static bool pa_sound_device_handle_reconnect(pa_handle *p, char *device_name, si
pa_mainloop_iterate(p->mainloop, 0, NULL);
- std::lock_guard<std::mutex> lock(p->reconnect_mutex);
+ pthread_mutex_lock(&p->reconnect_mutex);
p->reconnect = false;
+ pthread_mutex_unlock(&p->reconnect_mutex);
return true;
}
@@ -452,7 +472,7 @@ static int pa_sound_device_read(pa_handle *p, double timeout_seconds) {
p->read_data = NULL;
p->read_length = 0;
p->read_index = 0;
-
+
if(pa_stream_drop(p->stream) != 0)
goto fail;
@@ -469,27 +489,27 @@ static int pa_sound_device_read(pa_handle *p, double timeout_seconds) {
return success ? 0 : -1;
}
-static pa_sample_format_t audio_format_to_pulse_audio_format(AudioFormat audio_format) {
+static pa_sample_format_t audio_format_to_pulse_audio_format(gsr_audio_format audio_format) {
switch(audio_format) {
- case S16: return PA_SAMPLE_S16LE;
- case S32: return PA_SAMPLE_S32LE;
- case F32: return PA_SAMPLE_FLOAT32LE;
+ 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(AudioFormat audio_format) {
+static int audio_format_to_get_bytes_per_sample(gsr_audio_format audio_format) {
switch(audio_format) {
- case S16: return 2;
- case S32: return 4;
- case F32: return 4;
+ 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, AudioFormat audio_format) {
+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;
@@ -503,9 +523,9 @@ int sound_device_get_by_name(SoundDevice *device, const char *node_name, const c
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);
+ pa_handle *handle = pa_sound_device_new(NULL, 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;
}
@@ -543,7 +563,7 @@ void sound_device_flush(SoundDevice *device) {
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));
+ //gsr_log(GSR_LOG_LEVEL_ERROR, "pa_simple_read() failed: %s", pa_strerror(error));
*latency_seconds = 0.0;
return -1;
}
@@ -553,7 +573,7 @@ int sound_device_read_next_chunk(SoundDevice *device, void **buffer, double time
}
static void pa_state_cb(pa_context *c, void *userdata) {
- pa_context_state state = pa_context_get_state(c);
+ pa_context_state_t state = pa_context_get_state(c);
int *pa_ready = (int*)userdata;
switch(state) {
case PA_CONTEXT_UNCONNECTED:
@@ -572,29 +592,38 @@ static void pa_state_cb(pa_context *c, void *userdata) {
}
}
-static void pa_sourcelist_cb(pa_context*, const pa_source_info *source_info, int eol, void *userdata) {
+static void pa_sourcelist_cb(pa_context *ctx, const pa_source_info *source_info, int eol, void *userdata) {
+ (void)ctx;
if(eol > 0)
return;
- AudioDevices *audio_devices = (AudioDevices*)userdata;
- audio_devices->audio_inputs.push_back({ source_info->name, source_info->description });
+ 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*, const pa_server_info *server_info, void *userdata) {
- AudioDevices *audio_devices = (AudioDevices*)userdata;
+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)
- audio_devices->default_output = std::string(server_info->default_sink_name) + ".monitor";
+ snprintf(audio_devices->default_output, sizeof(audio_devices->default_output), "%s.monitor", server_info->default_sink_name);
if(server_info->default_source_name)
- audio_devices->default_input = 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*, const pa_server_info *server_info, void *userdata) {
+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(AudioDevices &audio_devices) {
+static void get_pulseaudio_default_inputs(gsr_audio_devices *audio_devices) {
int state = 0;
int pa_ready = 0;
pa_operation *pa_op = NULL;
@@ -618,7 +647,7 @@ static void get_pulseaudio_default_inputs(AudioDevices &audio_devices) {
switch(state) {
case 0: {
- pa_op = pa_context_get_server_info(ctx, pa_server_info_cb, &audio_devices);
+ pa_op = pa_context_get_server_info(ctx, pa_server_info_cb, audio_devices);
++state;
break;
}
@@ -639,8 +668,9 @@ static void get_pulseaudio_default_inputs(AudioDevices &audio_devices) {
pa_mainloop_free(main_loop);
}
-AudioDevices get_pulseaudio_inputs() {
- AudioDevices audio_devices;
+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;
@@ -650,7 +680,7 @@ AudioDevices get_pulseaudio_inputs() {
pa_mainloop *main_loop = pa_mainloop_new();
if(!main_loop)
- return audio_devices;
+ 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)
@@ -667,7 +697,7 @@ AudioDevices get_pulseaudio_inputs() {
switch(state) {
case 0: {
- pa_op = pa_context_get_source_info_list(ctx, pa_sourcelist_cb, &audio_devices);
+ pa_op = pa_context_get_source_info_list(ctx, pa_sourcelist_cb, audio_devices);
++state;
break;
}
@@ -686,10 +716,20 @@ AudioDevices get_pulseaudio_inputs() {
pa_context_disconnect(ctx);
pa_context_unref(ctx);
pa_mainloop_free(main_loop);
- return audio_devices;
}
-bool pulseaudio_server_is_pipewire() {
+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;
diff --git a/src/utils.c b/src/utils.c
index 31853e2..71ad02d 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;
}
}
@@ -388,8 +389,10 @@ 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 {
- 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;
}
@@ -449,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) {
@@ -656,7 +660,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,8 +681,94 @@ 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;
}
+
+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);
+}
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 <stdio.h>
#include <stdlib.h>
@@ -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/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 <stddef.h>
-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);
diff --git a/src/window/x11.c b/src/window/x11.c
index 3b3c955..2ae62c8 100644
--- a/src/window/x11.c
+++ b/src/window/x11.c
@@ -1,10 +1,10 @@
#include "../../include/window/x11.h"
+#include "../../include/log.h"
#include "../../include/vec2.h"
#include "../../include/defs.h"
#include "../../include/utils.h"
-#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
@@ -32,7 +32,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 +68,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;
}