#pragma once #include #include #include #include #include #include #include #include #include #include #include #include #include struct ConfigHotkey { int64_t keysym = 0; uint32_t modifiers = 0; }; struct MainConfig { std::string record_area_option; int32_t record_area_width = 0; int32_t record_area_height = 0; int32_t video_width = 0; int32_t video_height = 0; int32_t fps = 60; int32_t video_bitrate = 15000; bool merge_audio_tracks = true; bool record_app_audio_inverted = false; bool change_video_resolution = false; std::string audio_type_view = "audio_devices"; std::vector audio_input; std::vector application_audio; std::string color_range; std::string quality; std::string codec; // Video codec std::string audio_codec; std::string framerate_mode; bool advanced_view = false; bool overclock = false; bool show_recording_started_notifications = false; bool show_recording_stopped_notifications = false; bool show_recording_saved_notifications = true; bool record_cursor = true; bool hide_window_when_recording = false; bool software_encoding_warning_shown = false; bool steam_deck_warning_shown = false; bool hevc_amd_bug_warning_shown = false; bool av1_amd_bug_warning_shown = false; bool restore_portal_session = true; }; struct YoutubeStreamConfig { std::string stream_key; }; struct TwitchStreamConfig { std::string stream_key; }; struct CustomStreamConfig { std::string url; std::string container; }; struct StreamingConfig { std::string streaming_service; YoutubeStreamConfig youtube; TwitchStreamConfig twitch; CustomStreamConfig custom; ConfigHotkey start_stop_recording_hotkey; }; struct RecordConfig { std::string save_directory; std::string container; ConfigHotkey start_stop_recording_hotkey; ConfigHotkey pause_unpause_recording_hotkey; }; struct ReplayConfig { std::string save_directory; std::string container; int32_t replay_time = 30; ConfigHotkey start_stop_recording_hotkey; ConfigHotkey save_recording_hotkey; }; struct Config { MainConfig main_config; StreamingConfig streaming_config; RecordConfig record_config; ReplayConfig replay_config; }; typedef enum { CONFIG_TYPE_BOOL, CONFIG_TYPE_STRING, // std::string CONFIG_TYPE_I32, CONFIG_TYPE_HOTKEY, // ConfigHotkey CONFIG_TYPE_STRING_ARRAY, // std::vector } ConfigValueType; struct ConfigValue { ConfigValueType type; void *data; }; static 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 std::string get_config_dir() { std::string config_dir; const char *xdg_config_home = getenv("XDG_CONFIG_HOME"); if(xdg_config_home) { config_dir = xdg_config_home; } else { config_dir = get_home_dir() + "/.config"; } config_dir += "/gpu-screen-recorder"; return config_dir; } // Whoever designed xdg-user-dirs is retarded. Why are some XDG variables environment variables // while others are in this pseudo shell config file ~/.config/user-dirs.dirs static std::map get_xdg_variables() { std::string user_dirs_filepath; const char *xdg_config_home = getenv("XDG_CONFIG_HOME"); if(xdg_config_home) { user_dirs_filepath = xdg_config_home; } else { user_dirs_filepath = get_home_dir() + "/.config"; } user_dirs_filepath += "/user-dirs.dirs"; std::map result; FILE *f = fopen(user_dirs_filepath.c_str(), "rb"); if(!f) return result; char line[PATH_MAX]; while(fgets(line, sizeof(line), f)) { int len = strlen(line); if(len < 2) continue; if(line[0] == '#') continue; if(line[len - 1] == '\n') { line[len - 1] = '\0'; len--; } if(line[len - 1] != '"') continue; line[len - 1] = '\0'; len--; const char *sep = strchr(line, '='); if(!sep) continue; if(sep[1] != '\"') continue; std::string value(sep + 2); if(strncmp(value.c_str(), "$HOME/", 6) == 0) value = get_home_dir() + value.substr(5); std::string key(line, sep - line); result[std::move(key)] = std::move(value); } fclose(f); return result; } static std::string get_videos_dir() { auto xdg_vars = get_xdg_variables(); std::string xdg_videos_dir = xdg_vars["XDG_VIDEOS_DIR"]; if(xdg_videos_dir.empty()) xdg_videos_dir = get_home_dir() + "/Videos"; return xdg_videos_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 = nullptr; size_t size = 0; 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 std::map get_config_options(Config &config) { return { {"main.record_area_option", {CONFIG_TYPE_STRING, &config.main_config.record_area_option}}, {"main.record_area_width", {CONFIG_TYPE_I32, &config.main_config.record_area_width}}, {"main.record_area_height", {CONFIG_TYPE_I32, &config.main_config.record_area_height}}, {"main.video_width", {CONFIG_TYPE_I32, &config.main_config.video_width}}, {"main.video_height", {CONFIG_TYPE_I32, &config.main_config.video_height}}, {"main.fps", {CONFIG_TYPE_I32, &config.main_config.fps}}, {"main.video_bitrate", {CONFIG_TYPE_I32, &config.main_config.video_bitrate}}, {"main.merge_audio_tracks", {CONFIG_TYPE_BOOL, &config.main_config.merge_audio_tracks}}, {"main.record_app_audio_inverted", {CONFIG_TYPE_BOOL, &config.main_config.record_app_audio_inverted}}, {"main.change_video_resolution", {CONFIG_TYPE_BOOL, &config.main_config.change_video_resolution}}, {"main.audio_type_view", {CONFIG_TYPE_STRING, &config.main_config.audio_type_view}}, {"main.audio_input", {CONFIG_TYPE_STRING_ARRAY, &config.main_config.audio_input}}, {"main.application_audio", {CONFIG_TYPE_STRING_ARRAY, &config.main_config.application_audio}}, {"main.color_range", {CONFIG_TYPE_STRING, &config.main_config.color_range}}, {"main.quality", {CONFIG_TYPE_STRING, &config.main_config.quality}}, {"main.codec", {CONFIG_TYPE_STRING, &config.main_config.codec}}, {"main.audio_codec", {CONFIG_TYPE_STRING, &config.main_config.audio_codec}}, {"main.framerate_mode", {CONFIG_TYPE_STRING, &config.main_config.framerate_mode}}, {"main.advanced_view", {CONFIG_TYPE_BOOL, &config.main_config.advanced_view}}, {"main.overclock", {CONFIG_TYPE_BOOL, &config.main_config.overclock}}, {"main.show_recording_started_notifications", {CONFIG_TYPE_BOOL, &config.main_config.show_recording_started_notifications}}, {"main.show_recording_stopped_notifications", {CONFIG_TYPE_BOOL, &config.main_config.show_recording_stopped_notifications}}, {"main.show_recording_saved_notifications", {CONFIG_TYPE_BOOL, &config.main_config.show_recording_saved_notifications}}, {"main.record_cursor", {CONFIG_TYPE_BOOL, &config.main_config.record_cursor}}, {"main.hide_window_when_recording", {CONFIG_TYPE_BOOL, &config.main_config.hide_window_when_recording}}, {"main.software_encoding_warning_shown", {CONFIG_TYPE_BOOL, &config.main_config.software_encoding_warning_shown}}, {"main.steam_deck_warning_shown", {CONFIG_TYPE_BOOL, &config.main_config.steam_deck_warning_shown}}, {"main.hevc_amd_bug_warning_shown", {CONFIG_TYPE_BOOL, &config.main_config.hevc_amd_bug_warning_shown}}, {"main.av1_amd_bug_warning_shown", {CONFIG_TYPE_BOOL, &config.main_config.av1_amd_bug_warning_shown}}, {"main.restore_portal_session", {CONFIG_TYPE_BOOL, &config.main_config.restore_portal_session}}, {"streaming.service", {CONFIG_TYPE_STRING, &config.streaming_config.streaming_service}}, {"streaming.youtube.key", {CONFIG_TYPE_STRING, &config.streaming_config.youtube.stream_key}}, {"streaming.twitch.key", {CONFIG_TYPE_STRING, &config.streaming_config.twitch.stream_key}}, {"streaming.custom.url", {CONFIG_TYPE_STRING, &config.streaming_config.custom.url}}, {"streaming.custom.container", {CONFIG_TYPE_STRING, &config.streaming_config.custom.container}}, {"streaming.start_stop_recording_hotkey", {CONFIG_TYPE_HOTKEY, &config.streaming_config.start_stop_recording_hotkey}}, {"record.save_directory", {CONFIG_TYPE_STRING, &config.record_config.save_directory}}, {"record.container", {CONFIG_TYPE_STRING, &config.record_config.container}}, {"record.start_stop_recording_hotkey", {CONFIG_TYPE_HOTKEY, &config.record_config.start_stop_recording_hotkey}}, {"record.pause_unpause_recording_hotkey", {CONFIG_TYPE_HOTKEY, &config.record_config.pause_unpause_recording_hotkey}}, {"replay.save_directory", {CONFIG_TYPE_STRING, &config.replay_config.save_directory}}, {"replay.container", {CONFIG_TYPE_STRING, &config.replay_config.container}}, {"replay.time", {CONFIG_TYPE_I32, &config.replay_config.replay_time}}, {"replay.start_stop_recording_hotkey", {CONFIG_TYPE_HOTKEY, &config.replay_config.start_stop_recording_hotkey}}, {"replay.save_recording_hotkey", {CONFIG_TYPE_HOTKEY, &config.replay_config.save_recording_hotkey}} }; } #define FORMAT_I32 "%" PRIi32 #define FORMAT_I64 "%" PRIi64 #define FORMAT_U32 "%" PRIu32 static Config read_config(bool &config_empty) { Config config; const std::string config_path = get_config_dir() + "/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()); config_empty = true; return config; } auto config_options = get_config_options(config); string_split_char(file_content, '\n', [&](StringView line) { StringView key, sv_val; if(!config_split_key_value(line, key, sv_val)) { fprintf(stderr, "Warning: Invalid config option format: %.*s\n", (int)line.size, line.str); return true; } if(key.size == 0 || sv_val.size == 0) return true; auto it = config_options.find(std::string(key.str, key.size)); if(it == config_options.end()) return true; switch(it->second.type) { case CONFIG_TYPE_BOOL: { *(bool*)it->second.data = sv_val == "true"; break; } case CONFIG_TYPE_STRING: { ((std::string*)it->second.data)->assign(sv_val.str, sv_val.size); break; } case CONFIG_TYPE_I32: { std::string value_str(sv_val.str, sv_val.size); int32_t *value = (int32_t*)it->second.data; if(sscanf(value_str.c_str(), FORMAT_I32, value) != 1) { fprintf(stderr, "Warning: Invalid config option value for %.*s\n", (int)key.size, key.str); *value = 0; } break; } case CONFIG_TYPE_HOTKEY: { std::string value_str(sv_val.str, sv_val.size); ConfigHotkey *config_hotkey = (ConfigHotkey*)it->second.data; if(sscanf(value_str.c_str(), FORMAT_I64 " " FORMAT_U32, &config_hotkey->keysym, &config_hotkey->modifiers) != 2) { fprintf(stderr, "Warning: Invalid config option value for %.*s\n", (int)key.size, key.str); config_hotkey->keysym = 0; config_hotkey->modifiers = 0; } break; } case CONFIG_TYPE_STRING_ARRAY: { std::string array_value(sv_val.str, sv_val.size); ((std::vector*)it->second.data)->push_back(std::move(array_value)); break; } } return true; }); return config; } static void save_config(Config &config) { const std::string config_path = get_config_dir() + "/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; } const auto config_options = get_config_options(config); for(auto it : config_options) { switch(it.second.type) { case CONFIG_TYPE_BOOL: { fprintf(file, "%s %s\n", it.first.c_str(), *(bool*)it.second.data ? "true" : "false"); break; } case CONFIG_TYPE_STRING: { fprintf(file, "%s %s\n", it.first.c_str(), ((std::string*)it.second.data)->c_str()); break; } case CONFIG_TYPE_I32: { fprintf(file, "%s " FORMAT_I32 "\n", it.first.c_str(), *(int32_t*)it.second.data); break; } case CONFIG_TYPE_HOTKEY: { const ConfigHotkey *config_hotkey = (const ConfigHotkey*)it.second.data; fprintf(file, "%s " FORMAT_I64 " " FORMAT_U32 "\n", it.first.c_str(), config_hotkey->keysym, config_hotkey->modifiers); break; } case CONFIG_TYPE_STRING_ARRAY: { for(const std::string &value : *(std::vector*)it.second.data) { fprintf(file, "%s %s\n", it.first.c_str(), value.c_str()); } break; } } } fclose(file); }