From acaf9b978be9a4331a5229b2802149f7f4ff95ea Mon Sep 17 00:00:00 2001 From: dec05eba Date: Sun, 24 Jul 2022 18:14:56 +0200 Subject: Save options to config file --- src/config.hpp | 258 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/main.cpp | 175 ++++++++++++++++++++++++++------------ 2 files changed, 381 insertions(+), 52 deletions(-) create mode 100644 src/config.hpp diff --git a/src/config.hpp b/src/config.hpp new file mode 100644 index 0000000..d30710f --- /dev/null +++ b/src/config.hpp @@ -0,0 +1,258 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +struct MainConfig { + std::string record_area_option; + int fps = 60; + std::string audio_input; + std::string quality; +}; + +struct StreamingConfig { + std::string streaming_service; + std::string stream_key; +}; + +struct RecordConfig { + std::string save_directory; +}; + +struct ReplayConfig { + std::string save_directory; + int replay_time = 30; +}; + +struct Config { + MainConfig main_config; + StreamingConfig streaming_config; + RecordConfig record_config; + ReplayConfig replay_config; +}; + +std::string get_home_dir() { + const char *home_dir = getenv("HOME"); + if(!home_dir) { + passwd *pw = getpwuid(getuid()); + home_dir = pw->pw_dir; + } + + if(!home_dir) { + fprintf(stderr, "Error: Failed to get home directory of user, using /tmp directory\n"); + home_dir = "/tmp"; + } + + return home_dir; +} + +static int create_directory_recursive(char *path) { + int path_len = strlen(path); + char *p = path; + char *end = path + path_len; + for(;;) { + char *slash_p = strchr(p, '/'); + + // Skips first '/', we don't want to try and create the root directory + if(slash_p == path) { + ++p; + continue; + } + + if(!slash_p) + slash_p = end; + + char prev_char = *slash_p; + *slash_p = '\0'; + int err = mkdir(path, S_IRWXU); + *slash_p = prev_char; + + if(err == -1 && errno != EEXIST) + return err; + + if(slash_p == end) + break; + else + p = slash_p + 1; + } + return 0; +} + +static bool file_get_content(const char *filepath, std::string &file_content) { + file_content.clear(); + bool success = false; + + FILE *file = fopen(filepath, "rb"); + if(!file) + return success; + + fseek(file, 0, SEEK_END); + long file_size = ftell(file); + if(file_size != -1) { + file_content.resize(file_size); + fseek(file, 0, SEEK_SET); + if((long)fread(&file_content[0], 1, file_size, file) == file_size) + success = true; + } + + fclose(file); + return success; +} + +struct StringView { + const char *str; + size_t size; + + bool operator == (const char *other) const { + int len = strlen(other); + return (size_t)len == size && memcmp(str, other, size) == 0; + } + + size_t find(char c) const { + const void *p = memchr(str, c, size); + if(!p) + return std::string::npos; + return (const char*)p - str; + } +}; + +using StringSplitCallback = std::function; + +static void string_split_char(const std::string &str, char delimiter, StringSplitCallback callback_func) { + size_t index = 0; + while(index < str.size()) { + size_t new_index = str.find(delimiter, index); + if(new_index == std::string::npos) + new_index = str.size(); + + if(!callback_func({str.data() + index, new_index - index})) + break; + + index = new_index + 1; + } +} + +static bool config_split_key_value(const StringView str, StringView &key, StringView &value) { + key.str = nullptr; + key.size = 0; + + value.str = nullptr; + value.size = 0; + + size_t index = str.find(' '); + if(index == std::string::npos) + return std::string::npos; + + key.str = str.str; + key.size = index; + + value.str = str.str + index + 1; + value.size = str.size - (index + 1); + + return true; +} + +static bool string_to_int(std::string str, int &value) { + value = 0; + errno = 0; + char *endptr; + value = (int)strtoll(str.c_str(), &endptr, 10); + if(endptr == str.c_str() || errno != 0) + return false; + return true; +} + +static Config read_config() { + Config config; + + std::string config_path = get_home_dir(); + config_path += "/.config/gpu-screen-recorder/config"; + + std::string file_content; + if(!file_get_content(config_path.c_str(), file_content)) { + fprintf(stderr, "Warning: Failed to read config file: %s\n", config_path.c_str()); + return config; + } + + string_split_char(file_content, '\n', [&](StringView line) { + StringView key, value; + if(!config_split_key_value(line, key, value)) { + fprintf(stderr, "Warning: Invalid config option format: %.*s\n", (int)line.size, line.str); + return true; + } + + if(key == "main.record_area_option") { + config.main_config.record_area_option.assign(value.str, value.size); + } else if(key == "main.fps") { + if(!string_to_int(std::string(value.str, value.size), config.main_config.fps)) { + fprintf(stderr, "Warning: Invalid config option main.fps\n"); + config.main_config.fps = 60; + } + } else if(key == "main.audio_input") { + config.main_config.audio_input.assign(value.str, value.size); + } else if(key == "main.quality") { + config.main_config.quality.assign(value.str, value.size); + } else if(key == "streaming.service") { + config.streaming_config.streaming_service.assign(value.str, value.size); + } else if(key == "streaming.key") { + config.streaming_config.stream_key.assign(value.str, value.size); + } else if(key == "record.save_directory") { + config.record_config.save_directory.assign(value.str, value.size); + } else if(key == "replay.save_directory") { + config.replay_config.save_directory.assign(value.str, value.size); + } else if(key == "replay.time") { + if(!string_to_int(std::string(value.str, value.size), config.replay_config.replay_time)) { + fprintf(stderr, "Warning: Invalid config option replay.time\n"); + config.replay_config.replay_time = 30; + } + } else { + fprintf(stderr, "Warning: Invalid config option: %.*s\n", (int)line.size, line.str); + } + + return true; + }); + + return config; +} + +static void save_config(const Config &config) { + std::string config_path = get_home_dir(); + config_path += "/.config/gpu-screen-recorder/config"; + + char dir_tmp[PATH_MAX]; + strcpy(dir_tmp, config_path.c_str()); + char *dir = dirname(dir_tmp); + + if(create_directory_recursive(dir) != 0) { + fprintf(stderr, "Warning: Failed to create config directory: %s\n", dir); + return; + } + + FILE *file = fopen(config_path.c_str(), "wb"); + if(!file) { + fprintf(stderr, "Warning: Failed to create config file: %s\n", config_path.c_str()); + return; + } + + fprintf(file, "main.record_area_option %s\n", config.main_config.record_area_option.c_str()); + fprintf(file, "main.fps %d\n", config.main_config.fps); + fprintf(file, "main.audio_input %s\n", config.main_config.audio_input.c_str()); + fprintf(file, "main.quality %s\n", config.main_config.quality.c_str()); + + fprintf(file, "streaming.service %s\n", config.streaming_config.streaming_service.c_str()); + fprintf(file, "streaming.key %s\n", config.streaming_config.stream_key.c_str()); + + fprintf(file, "record.save_directory %s\n", config.record_config.save_directory.c_str()); + + fprintf(file, "replay.save_directory %s\n", config.replay_config.save_directory.c_str()); + fprintf(file, "replay.time %d\n", config.replay_config.replay_time); + + fclose(file); +} \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp index 4d38ad1..53c733b 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,3 +1,4 @@ +#include "config.hpp" #include #include #include @@ -13,8 +14,6 @@ #include #include #include -#include -#include #include #include @@ -65,16 +64,15 @@ static bool replaying = false; static bool recording = false; static bool streaming = false; static pid_t gpu_screen_recorder_process = -1; - -static const char* get_home_dir() { - const char *home_dir = getenv("HOME"); - if(!home_dir) { - passwd *pw = getpwuid(getuid()); - home_dir = pw->pw_dir; - } - if(!home_dir) - home_dir = "/tmp"; - return home_dir; +static Config config; + +static bool is_directory(const char *filepath) { + struct stat file_stat; + memset(&file_stat, 0, sizeof(file_stat)); + int ret = stat(filepath, &file_stat); + if(ret == 0) + return S_ISDIR(file_stat.st_mode); + return false; } static std::string get_date_str() { @@ -85,38 +83,6 @@ static std::string get_date_str() { return str; } -static int create_directory_recursive(char *path) { - int path_len = strlen(path); - char *p = path; - char *end = path + path_len; - for(;;) { - char *slash_p = strchr(p, '/'); - - // Skips first '/', we don't want to try and create the root directory - if(slash_p == path) { - ++p; - continue; - } - - if(!slash_p) - slash_p = end; - - char prev_char = *slash_p; - *slash_p = '\0'; - int err = mkdir(path, S_IRWXU); - *slash_p = prev_char; - - if(err == -1 && errno != EEXIST) - return err; - - if(slash_p == end) - break; - else - p = slash_p + 1; - } - return 0; -} - static const XRRModeInfo* get_mode_info(const XRRScreenResources *sr, RRMode id) { for(int i = 0; i < sr->nmode; ++i) { if(sr->modes[i].id == id) @@ -358,8 +324,8 @@ static gboolean on_start_recording_click(GtkButton *button, gpointer userdata) { PageNavigationUserdata *page_navigation_userdata = (PageNavigationUserdata*)userdata; gtk_stack_set_visible_child(page_navigation_userdata->stack, page_navigation_userdata->recording_page); - std::string video_filepath = get_home_dir(); - video_filepath += "/Videos/Video_" + get_date_str() + ".mp4"; + std::string video_filepath = config.record_config.save_directory; + video_filepath += "/Video_" + get_date_str() + ".mp4"; gtk_button_set_label(file_chooser_button, video_filepath.c_str()); return true; } @@ -788,14 +754,21 @@ static void pa_state_cb(pa_context *c, void *userdata) { } } +struct AudioInput { + std::string name; + std::string description; +}; + static void pa_sourcelist_cb(pa_context *ctx, const pa_source_info *source_info, int eol, void *userdata) { if(eol > 0) return; - gtk_combo_box_text_append(audio_input_menu, source_info->name, source_info->description); + std::vector *inputs = (std::vector*)userdata; + inputs->push_back({ source_info->name, source_info->description }); } -static void populate_audio_input_menu_with_pulseaudio_monitors() { +static std::vector get_pulseaudio_inputs() { + std::vector inputs; pa_mainloop *main_loop = pa_mainloop_new(); pa_context *ctx = pa_context_new(pa_mainloop_get_api(main_loop), "gpu-screen-recorder-gtk"); @@ -815,7 +788,7 @@ static void populate_audio_input_menu_with_pulseaudio_monitors() { switch(state) { case 0: { - pa_op = pa_context_get_source_info_list(ctx, pa_sourcelist_cb, nullptr); + pa_op = pa_context_get_source_info_list(ctx, pa_sourcelist_cb, &inputs); ++state; break; } @@ -828,7 +801,7 @@ static void populate_audio_input_menu_with_pulseaudio_monitors() { pa_context_disconnect(ctx); pa_context_unref(ctx); pa_mainloop_free(main_loop); - return; + return inputs; } pa_mainloop_iterate(main_loop, 1, NULL); @@ -970,7 +943,9 @@ static GtkWidget* create_common_settings_page(GtkStack *stack, GtkApplication *a gtk_grid_attach(audio_grid, gtk_label_new("Audio input: "), 0, 0, 1, 1); audio_input_menu = GTK_COMBO_BOX_TEXT(gtk_combo_box_text_new()); gtk_combo_box_text_append(audio_input_menu, "None", "None"); - populate_audio_input_menu_with_pulseaudio_monitors(); + for(const AudioInput &audio_input : get_pulseaudio_inputs()) { + gtk_combo_box_text_append(audio_input_menu, audio_input.name.c_str(), audio_input.description.c_str()); + } g_signal_connect(audio_input_menu, "changed", G_CALLBACK(audio_input_change_callback), nullptr); gtk_widget_set_hexpand(GTK_WIDGET(audio_input_menu), true); gtk_grid_attach(audio_grid, GTK_WIDGET(audio_input_menu), 1, 0, 1, 1); @@ -1168,6 +1143,28 @@ static gboolean on_destroy_window(GtkWidget *widget, GdkEvent *event, gpointer d /* Ignore... */ } } + + const gchar *record_filename = gtk_button_get_label(file_chooser_button); + + char dir_tmp[PATH_MAX]; + strcpy(dir_tmp, record_filename); + char *record_dir = dirname(dir_tmp); + + config.main_config.record_area_option = gtk_combo_box_get_active_id(GTK_COMBO_BOX(record_area_selection_menu)); + config.main_config.fps = gtk_spin_button_get_value_as_int(fps_entry); + config.main_config.audio_input = gtk_combo_box_get_active_id(GTK_COMBO_BOX(audio_input_menu)); + config.main_config.quality = gtk_combo_box_get_active_id(GTK_COMBO_BOX(quality_input_menu)); + + config.streaming_config.streaming_service = gtk_combo_box_get_active_id(GTK_COMBO_BOX(stream_service_input_menu)); + config.streaming_config.stream_key = gtk_entry_get_text(stream_id_entry); + + config.record_config.save_directory = record_dir; + + config.replay_config.save_directory = gtk_button_get_label(replay_file_chooser_button); + config.replay_config.replay_time = gtk_spin_button_get_value_as_int(replay_time_entry); + + save_config(config); + return true; } @@ -1258,7 +1255,80 @@ static gboolean handle_child_process_death(gpointer userdata) { return G_SOURCE_CONTINUE; } -static void activate(GtkApplication *app, gpointer userdata) { +static void load_config() { + config = read_config(); + + if(strcmp(config.main_config.record_area_option.c_str(), "window") == 0) { + // + } else if(strcmp(config.main_config.record_area_option.c_str(), "focused") == 0) { + // + } else if(strcmp(config.main_config.record_area_option.c_str(), "screen") == 0 || strcmp(config.main_config.record_area_option.c_str(), "screen-direct") == 0) { + // + } else { + bool found_monitor = false; + int monitor_name_size = strlen(config.main_config.record_area_option.c_str()); + for_each_active_monitor_output(gdk_x11_get_default_xdisplay(), [&](const XRROutputInfo *output_info, const XRRCrtcInfo *crtc_info, const XRRModeInfo*) { + if(monitor_name_size == output_info->nameLen && strncmp(config.main_config.record_area_option.c_str(), output_info->name, output_info->nameLen) == 0) { + found_monitor = true; + } + }); + + if(!found_monitor) + config.main_config.record_area_option = "window"; + } + + if(config.main_config.fps < 1) + config.main_config.fps = 1; + else if(config.main_config.fps > 5000) + config.main_config.fps = 5000; + + if(config.main_config.audio_input != "None") { + bool found_audio_input = false; + for(const AudioInput &audio_input : get_pulseaudio_inputs()) { + if(config.main_config.audio_input == audio_input.name) { + found_audio_input = true; + break; + } + } + + if(!found_audio_input) + config.main_config.audio_input = "None"; + } + + if(config.main_config.quality != "medium" && config.main_config.quality != "high" && config.main_config.quality != "ultra") + config.main_config.quality = "medium"; + + if(config.streaming_config.streaming_service != "twitch" && config.streaming_config.streaming_service != "youtube") + config.streaming_config.streaming_service = "twitch"; + + if(config.record_config.save_directory.empty() || !is_directory(config.record_config.save_directory.c_str())) + config.record_config.save_directory = get_home_dir() + "/Videos"; + + if(config.replay_config.save_directory.empty() || !is_directory(config.replay_config.save_directory.c_str())) + config.replay_config.save_directory = get_home_dir() + "/Videos"; + + if(config.replay_config.replay_time < 5) + config.replay_config.replay_time = 5; + else if(config.replay_config.replay_time> 1200) + config.replay_config.replay_time = 1200; + + gtk_combo_box_set_active_id(GTK_COMBO_BOX(record_area_selection_menu), config.main_config.record_area_option.c_str()); + gtk_spin_button_set_value(fps_entry, config.main_config.fps); + gtk_combo_box_set_active_id(GTK_COMBO_BOX(audio_input_menu), config.main_config.audio_input.c_str()); + gtk_combo_box_set_active_id(GTK_COMBO_BOX(quality_input_menu), config.main_config.quality.c_str()); + + gtk_combo_box_set_active_id(GTK_COMBO_BOX(stream_service_input_menu), config.streaming_config.streaming_service.c_str()); + gtk_entry_set_text(stream_id_entry, config.streaming_config.stream_key.c_str()); + + std::string video_filepath = config.record_config.save_directory; + video_filepath += "/Video_" + get_date_str() + ".mp4"; + gtk_button_set_label(file_chooser_button, video_filepath.c_str()); + + gtk_button_set_label(replay_file_chooser_button, config.replay_config.save_directory.c_str()); + gtk_spin_button_set_value(replay_time_entry, config.replay_config.replay_time); +} + +static void activate(GtkApplication *app, gpointer userdata) { GtkWidget *window = gtk_application_window_new(app); g_signal_connect(window, "destroy", G_CALLBACK(on_destroy_window), nullptr); gtk_window_set_title(GTK_WINDOW(window), "GPU Screen Recorder"); @@ -1304,6 +1374,7 @@ static void activate(GtkApplication *app, gpointer userdata) { g_timeout_add(1000, handle_child_process_death, app); + load_config(); gtk_widget_show_all(window); } -- cgit v1.2.3