diff options
Diffstat (limited to 'src')
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(¶ms, &nvenc_encoder) != NV_ENC_SUCCESS) { - fprintf(stderr, "gsr error: gsr_get_supported_video_codecs_nvenc: nvEncOpenEncodeSessionEx (retry) failed\n"); + gsr_log(GSR_LOG_LEVEL_ERROR, "gsr_get_supported_video_codecs_nvenc: nvEncOpenEncodeSessionEx (retry) failed"); goto done; } } diff --git a/src/codec_query/vaapi.c b/src/codec_query/vaapi.c index 0943450..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; } @@ -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; } @@ -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; } @@ -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(¶ms); - 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(¶ms); - return video_encoder; - } - - switch(egl->gpu_info.vendor) { - case GSR_GPU_VENDOR_AMD: - case GSR_GPU_VENDOR_INTEL: - case GSR_GPU_VENDOR_BROADCOM: { - gsr_video_encoder_vaapi_params params; - params.egl = egl; - params.color_depth = color_depth; - video_encoder = gsr_video_encoder_vaapi_create(¶ms); - break; - } - case GSR_GPU_VENDOR_NVIDIA: { - gsr_video_encoder_nvenc_params params; - params.egl = egl; - params.color_depth = color_depth; - video_encoder = gsr_video_encoder_nvenc_create(¶ms); - break; - } - } - - return video_encoder; -} - -static bool get_supported_video_codecs(gsr_egl *egl, gsr_video_codec video_codec, bool use_software_video_encoder, bool cleanup, gsr_supported_video_codecs *video_codecs) { - memset(video_codecs, 0, sizeof(*video_codecs)); - - if(use_software_video_encoder) { - video_codecs->h264.supported = avcodec_find_encoder_by_name("libx264"); - video_codecs->h264.max_resolution = {4096, 2304}; - return true; - } - - if(video_codec_is_vulkan(video_codec)) - return gsr_get_supported_video_codecs_vulkan(video_codecs, egl->card_path, &egl->vulkan_device_index, cleanup); - - switch(egl->gpu_info.vendor) { - case GSR_GPU_VENDOR_AMD: - case GSR_GPU_VENDOR_INTEL: - case GSR_GPU_VENDOR_BROADCOM: - return gsr_get_supported_video_codecs_vaapi(video_codecs, egl->card_path, cleanup); - case GSR_GPU_VENDOR_NVIDIA: - return gsr_get_supported_video_codecs_nvenc(video_codecs, cleanup); - } - - return false; -} - -static void xwayland_check_callback(const gsr_monitor *monitor, void *userdata) { - bool *xwayland_found = (bool*)userdata; - if(monitor->name_len >= 8 && strncmp(monitor->name, "XWAYLAND", 8) == 0) - *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(¶ms); + return video_encoder; + } + + if(video_codec_is_vulkan(settings->video_codec)) { + gsr_video_encoder_vulkan_params params; + params.egl = egl; + params.color_depth = color_depth; + video_encoder = gsr_video_encoder_vulkan_create(¶ms); + return video_encoder; + } + + switch(egl->gpu_info.vendor) { + case GSR_GPU_VENDOR_AMD: + case GSR_GPU_VENDOR_INTEL: + case GSR_GPU_VENDOR_BROADCOM: + 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(¶ms); + break; + } + case GSR_GPU_VENDOR_NVIDIA: { + gsr_video_encoder_nvenc_params params; + params.egl = egl; + params.color_depth = color_depth; + video_encoder = gsr_video_encoder_nvenc_create(¶ms); + break; + } + } + + return video_encoder; +} + +bool get_supported_video_codecs(gsr_egl *egl, gsr_video_codec video_codec, bool use_software_video_encoder, bool cleanup, gsr_supported_video_codecs *video_codecs) { + memset(video_codecs, 0, sizeof(*video_codecs)); + + if(use_software_video_encoder) { + video_codecs->h264.supported = avcodec_find_encoder_by_name("libx264"); + video_codecs->h264.max_resolution = (vec2i){4096, 2304}; + return true; + } + + if(video_codec_is_vulkan(video_codec)) + return gsr_get_supported_video_codecs_vulkan(video_codecs, egl->card_path, &egl->vulkan_device_index, cleanup); + + switch(egl->gpu_info.vendor) { + case GSR_GPU_VENDOR_AMD: + case GSR_GPU_VENDOR_INTEL: + case GSR_GPU_VENDOR_BROADCOM: + 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 = ¶ms->windowing->egl; + self->window = params->windowing->window; + self->capture_deps = params->capture_deps; + self->capture_sources = params->capture_sources; + self->audio_input_tracks = params->audio_input_tracks; + 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; } |
