From 607d37b4235a998580e1629e4b4f7124d8adacec Mon Sep 17 00:00:00 2001 From: PatTheMav Date: Tue, 3 Sep 2024 16:29:30 +0200 Subject: [PATCH] UI: Rewrite profile system to enable user-provided storage location This change enables loading profiles from locations different than OBS' own configuration directory. It also rewrites profile management in the app to work off an in-memory collection of profiles found on disk and does not require iterating over directory contents for most profile interactions by the app. --- UI/api-interface.cpp | 690 +++++++------- UI/obs-app.cpp | 239 ++--- UI/window-basic-auto-config.cpp | 22 +- UI/window-basic-main-outputs.cpp | 19 +- UI/window-basic-main-profiles.cpp | 1464 +++++++++++++++-------------- UI/window-basic-main.cpp | 709 ++++++++------ UI/window-basic-main.hpp | 107 ++- UI/window-basic-settings.cpp | 45 +- UI/window-youtube-actions.cpp | 78 +- 9 files changed, 1772 insertions(+), 1601 deletions(-) diff --git a/UI/api-interface.cpp b/UI/api-interface.cpp index b41945f42..742e10515 100644 --- a/UI/api-interface.cpp +++ b/UI/api-interface.cpp @@ -16,7 +16,6 @@ template static T GetOBSRef(QListWidgetItem *item) return item->data(static_cast(QtDataRole::OBSRef)).value(); } -void EnumProfiles(function &&cb); void EnumSceneCollections(function &&cb); extern volatile bool streaming_active; @@ -218,29 +217,24 @@ struct OBSStudioAPI : obs_frontend_callbacks { void obs_frontend_get_profiles(std::vector &strings) override { - auto addProfile = [&](const char *name, const char *) { - strings.emplace_back(name); - return true; - }; + const OBSProfileCache &profiles = main->GetProfileCache(); - EnumProfiles(addProfile); + for (auto &[profileName, profile] : profiles) { + strings.emplace_back(profileName); + } } char *obs_frontend_get_current_profile(void) override { - const char *name = config_get_string(App()->GlobalConfig(), - "Basic", "Profile"); - return bstrdup(name); + const OBSProfile &profile = main->GetCurrentProfile(); + return bstrdup(profile.name.c_str()); } char *obs_frontend_get_current_profile_path(void) override { - char profilePath[512]; - int ret = GetProfilePath(profilePath, sizeof(profilePath), ""); - if (ret <= 0) - return nullptr; + const OBSProfile &profile = main->GetCurrentProfile(); - return bstrdup(profilePath); + return bstrdup(profile.path.u8string().c_str()); } void obs_frontend_set_current_profile(const char *profile) override @@ -510,353 +504,323 @@ struct OBSStudioAPI : obs_frontend_callbacks { config_t *obs_frontend_get_profile_config(void) override { - return main->basicConfig; - config_t *obs_frontend_get_global_config(void) override - { - blog(LOG_WARNING, - "DEPRECATION: obs_frontend_get_global_config is deprecated. Read from global or user configuration explicitly instead."); - return App()->GetAppConfig(); - } - - config_t *obs_frontend_get_app_config(void) override - { - return App()->GetAppConfig(); - } - - config_t *obs_frontend_get_user_config(void) override - { - return App()->GetUserConfig(); - } - - void obs_frontend_open_projector(const char *type, int monitor, - const char *geometry, - const char *name) override - { - SavedProjectorInfo proj = { - ProjectorType::Preview, - monitor, - geometry ? geometry : "", - name ? name : "", - }; - if (type) { - if (astrcmpi(type, "Source") == 0) - proj.type = ProjectorType::Source; - else if (astrcmpi(type, "Scene") == 0) - proj.type = ProjectorType::Scene; - else if (astrcmpi(type, "StudioProgram") == 0) - proj.type = - ProjectorType::StudioProgram; - else if (astrcmpi(type, "Multiview") == 0) - proj.type = ProjectorType::Multiview; - } - QMetaObject::invokeMethod( - main, "OpenSavedProjector", WaitConnection(), - Q_ARG(SavedProjectorInfo *, &proj)); - } - - void obs_frontend_save(void) override - { - main->SaveProject(); - } - - void obs_frontend_defer_save_begin(void) override - { - QMetaObject::invokeMethod(main, "DeferSaveBegin"); - } - - void obs_frontend_defer_save_end(void) override - { - QMetaObject::invokeMethod(main, "DeferSaveEnd"); - } - - void obs_frontend_add_save_callback( - obs_frontend_save_cb callback, void *private_data) - override - { - size_t idx = GetCallbackIdx(saveCallbacks, callback, - private_data); - if (idx == (size_t)-1) - saveCallbacks.emplace_back(callback, - private_data); - } - - void obs_frontend_remove_save_callback( - obs_frontend_save_cb callback, void *private_data) - override - { - size_t idx = GetCallbackIdx(saveCallbacks, callback, - private_data); - if (idx == (size_t)-1) - return; - - saveCallbacks.erase(saveCallbacks.begin() + idx); - } - - void obs_frontend_add_preload_callback( - obs_frontend_save_cb callback, void *private_data) - override - { - size_t idx = GetCallbackIdx(preloadCallbacks, callback, - private_data); - if (idx == (size_t)-1) - preloadCallbacks.emplace_back(callback, - private_data); - } - - void obs_frontend_remove_preload_callback( - obs_frontend_save_cb callback, void *private_data) - override - { - size_t idx = GetCallbackIdx(preloadCallbacks, callback, - private_data); - if (idx == (size_t)-1) - return; - - preloadCallbacks.erase(preloadCallbacks.begin() + idx); - } - - void obs_frontend_push_ui_translation( - obs_frontend_translate_ui_cb translate) override - { - App()->PushUITranslation(translate); - } - - void obs_frontend_pop_ui_translation(void) override - { - App()->PopUITranslation(); - } - - void obs_frontend_set_streaming_service(obs_service_t * service) - override - { - main->SetService(service); - } - - obs_service_t *obs_frontend_get_streaming_service(void) override - { - return main->GetService(); - } - - void obs_frontend_save_streaming_service(void) override - { - main->SaveService(); - } - - bool obs_frontend_preview_program_mode_active(void) override - { - return main->IsPreviewProgramMode(); - } - - void obs_frontend_set_preview_program_mode(bool enable) override - { - main->SetPreviewProgramMode(enable); - } - - void obs_frontend_preview_program_trigger_transition(void) - override - { - QMetaObject::invokeMethod(main, "TransitionClicked"); - } - - bool obs_frontend_preview_enabled(void) override - { - return main->previewEnabled; - } - - void obs_frontend_set_preview_enabled(bool enable) override - { - if (main->previewEnabled != enable) - main->EnablePreviewDisplay(enable); - } - - obs_source_t *obs_frontend_get_current_preview_scene(void) - override - { - if (main->IsPreviewProgramMode()) { - OBSSource source = - main->GetCurrentSceneSource(); - return obs_source_get_ref(source); - } - - return nullptr; - } - - void obs_frontend_set_current_preview_scene(obs_source_t * - scene) override - { - if (main->IsPreviewProgramMode()) { - QMetaObject::invokeMethod( - main, "SetCurrentScene", - Q_ARG(OBSSource, OBSSource(scene)), - Q_ARG(bool, false)); - } - } - - void obs_frontend_take_screenshot(void) override - { - QMetaObject::invokeMethod(main, "Screenshot"); - } - - void obs_frontend_take_source_screenshot(obs_source_t * source) - override - { - QMetaObject::invokeMethod(main, "Screenshot", - Q_ARG(OBSSource, - OBSSource(source))); - } - - obs_output_t *obs_frontend_get_virtualcam_output(void) override - { - OBSOutput output = - main->outputHandler->virtualCam.Get(); - return obs_output_get_ref(output); - } - - void obs_frontend_start_virtualcam(void) override - { - QMetaObject::invokeMethod(main, "StartVirtualCam"); - } - - void obs_frontend_stop_virtualcam(void) override - { - QMetaObject::invokeMethod(main, "StopVirtualCam"); - } - - bool obs_frontend_virtualcam_active(void) override - { - return os_atomic_load_bool(&virtualcam_active); - } - - void obs_frontend_reset_video(void) override - { - main->ResetVideo(); - } - - void obs_frontend_open_source_properties(obs_source_t * source) - override - { - QMetaObject::invokeMethod(main, "OpenProperties", - Q_ARG(OBSSource, - OBSSource(source))); - } - - void obs_frontend_open_source_filters(obs_source_t * source) - override - { - QMetaObject::invokeMethod(main, "OpenFilters", - Q_ARG(OBSSource, - OBSSource(source))); - } - - void obs_frontend_open_source_interaction(obs_source_t * source) - override - { - QMetaObject::invokeMethod(main, "OpenInteraction", - Q_ARG(OBSSource, - OBSSource(source))); - } - - void obs_frontend_open_sceneitem_edit_transform( - obs_sceneitem_t * item) override - { - QMetaObject::invokeMethod(main, "OpenEditTransform", - Q_ARG(OBSSceneItem, - OBSSceneItem(item))); - } - - char *obs_frontend_get_current_record_output_path(void) override - { - const char *recordOutputPath = - main->GetCurrentOutputPath(); - - return bstrdup(recordOutputPath); - } - - const char *obs_frontend_get_locale_string(const char *string) - override - { - return Str(string); - } - - bool obs_frontend_is_theme_dark(void) override - { - return App()->IsThemeDark(); - } - - char *obs_frontend_get_last_recording(void) override - { - return bstrdup( - main->outputHandler->lastRecordingPath.c_str()); - } - - char *obs_frontend_get_last_screenshot(void) override - { - return bstrdup(main->lastScreenshot.c_str()); - } - - char *obs_frontend_get_last_replay(void) override - { - return bstrdup(main->lastReplay.c_str()); - } - - void obs_frontend_add_undo_redo_action( - const char *name, const undo_redo_cb undo, - const undo_redo_cb redo, const char *undo_data, - const char *redo_data, bool repeatable) override - { - main->undo_s.add_action( - name, - [undo](const std::string &data) { - undo(data.c_str()); - }, - [redo](const std::string &data) { - redo(data.c_str()); - }, - undo_data, redo_data, repeatable); - } - - void on_load(obs_data_t * settings) override - { - for (size_t i = saveCallbacks.size(); i > 0; i--) { - auto cb = saveCallbacks[i - 1]; - cb.callback(settings, false, cb.private_data); - } - } - - void on_preload(obs_data_t * settings) override - { - for (size_t i = preloadCallbacks.size(); i > 0; i--) { - auto cb = preloadCallbacks[i - 1]; - cb.callback(settings, false, cb.private_data); - } - } - - void on_save(obs_data_t * settings) override - { - for (size_t i = saveCallbacks.size(); i > 0; i--) { - auto cb = saveCallbacks[i - 1]; - cb.callback(settings, true, cb.private_data); - } - } - - void on_event(enum obs_frontend_event event) override - { - if (main->disableSaving && - event != - OBS_FRONTEND_EVENT_SCENE_COLLECTION_CLEANUP && - event != OBS_FRONTEND_EVENT_EXIT) - return; - - for (size_t i = callbacks.size(); i > 0; i--) { - auto cb = callbacks[i - 1]; - cb.callback(event, cb.private_data); - } - } - }; - - obs_frontend_callbacks *InitializeAPIInterface(OBSBasic *main) - { - obs_frontend_callbacks *api = new OBSStudioAPI(main); - obs_frontend_set_callbacks_internal(api); - return api; + return main->activeConfiguration; } + + config_t *obs_frontend_get_global_config(void) override + { + blog(LOG_WARNING, + "DEPRECATION: obs_frontend_get_global_config is deprecated. Read from global or user configuration explicitly instead."); + return App()->GetAppConfig(); + } + + config_t *obs_frontend_get_app_config(void) override + { + return App()->GetAppConfig(); + } + + config_t *obs_frontend_get_user_config(void) override + { + return App()->GetUserConfig(); + } + + void obs_frontend_open_projector(const char *type, int monitor, + const char *geometry, + const char *name) override + { + SavedProjectorInfo proj = { + ProjectorType::Preview, + monitor, + geometry ? geometry : "", + name ? name : "", + }; + if (type) { + if (astrcmpi(type, "Source") == 0) + proj.type = ProjectorType::Source; + else if (astrcmpi(type, "Scene") == 0) + proj.type = ProjectorType::Scene; + else if (astrcmpi(type, "StudioProgram") == 0) + proj.type = ProjectorType::StudioProgram; + else if (astrcmpi(type, "Multiview") == 0) + proj.type = ProjectorType::Multiview; + } + QMetaObject::invokeMethod(main, "OpenSavedProjector", + WaitConnection(), + Q_ARG(SavedProjectorInfo *, &proj)); + } + + void obs_frontend_save(void) override { main->SaveProject(); } + + void obs_frontend_defer_save_begin(void) override + { + QMetaObject::invokeMethod(main, "DeferSaveBegin"); + } + + void obs_frontend_defer_save_end(void) override + { + QMetaObject::invokeMethod(main, "DeferSaveEnd"); + } + + void obs_frontend_add_save_callback(obs_frontend_save_cb callback, + void *private_data) override + { + size_t idx = + GetCallbackIdx(saveCallbacks, callback, private_data); + if (idx == (size_t)-1) + saveCallbacks.emplace_back(callback, private_data); + } + + void obs_frontend_remove_save_callback(obs_frontend_save_cb callback, + void *private_data) override + { + size_t idx = + GetCallbackIdx(saveCallbacks, callback, private_data); + if (idx == (size_t)-1) + return; + + saveCallbacks.erase(saveCallbacks.begin() + idx); + } + + void obs_frontend_add_preload_callback(obs_frontend_save_cb callback, + void *private_data) override + { + size_t idx = GetCallbackIdx(preloadCallbacks, callback, + private_data); + if (idx == (size_t)-1) + preloadCallbacks.emplace_back(callback, private_data); + } + + void obs_frontend_remove_preload_callback(obs_frontend_save_cb callback, + void *private_data) override + { + size_t idx = GetCallbackIdx(preloadCallbacks, callback, + private_data); + if (idx == (size_t)-1) + return; + + preloadCallbacks.erase(preloadCallbacks.begin() + idx); + } + + void obs_frontend_push_ui_translation( + obs_frontend_translate_ui_cb translate) override + { + App()->PushUITranslation(translate); + } + + void obs_frontend_pop_ui_translation(void) override + { + App()->PopUITranslation(); + } + + void obs_frontend_set_streaming_service(obs_service_t *service) override + { + main->SetService(service); + } + + obs_service_t *obs_frontend_get_streaming_service(void) override + { + return main->GetService(); + } + + void obs_frontend_save_streaming_service(void) override + { + main->SaveService(); + } + + bool obs_frontend_preview_program_mode_active(void) override + { + return main->IsPreviewProgramMode(); + } + + void obs_frontend_set_preview_program_mode(bool enable) override + { + main->SetPreviewProgramMode(enable); + } + + void obs_frontend_preview_program_trigger_transition(void) override + { + QMetaObject::invokeMethod(main, "TransitionClicked"); + } + + bool obs_frontend_preview_enabled(void) override + { + return main->previewEnabled; + } + + void obs_frontend_set_preview_enabled(bool enable) override + { + if (main->previewEnabled != enable) + main->EnablePreviewDisplay(enable); + } + + obs_source_t *obs_frontend_get_current_preview_scene(void) override + { + if (main->IsPreviewProgramMode()) { + OBSSource source = main->GetCurrentSceneSource(); + return obs_source_get_ref(source); + } + + return nullptr; + } + + void + obs_frontend_set_current_preview_scene(obs_source_t *scene) override + { + if (main->IsPreviewProgramMode()) { + QMetaObject::invokeMethod(main, "SetCurrentScene", + Q_ARG(OBSSource, + OBSSource(scene)), + Q_ARG(bool, false)); + } + } + + void obs_frontend_take_screenshot(void) override + { + QMetaObject::invokeMethod(main, "Screenshot"); + } + + void obs_frontend_take_source_screenshot(obs_source_t *source) override + { + QMetaObject::invokeMethod(main, "Screenshot", + Q_ARG(OBSSource, OBSSource(source))); + } + + obs_output_t *obs_frontend_get_virtualcam_output(void) override + { + OBSOutput output = main->outputHandler->virtualCam.Get(); + return obs_output_get_ref(output); + } + + void obs_frontend_start_virtualcam(void) override + { + QMetaObject::invokeMethod(main, "StartVirtualCam"); + } + + void obs_frontend_stop_virtualcam(void) override + { + QMetaObject::invokeMethod(main, "StopVirtualCam"); + } + + bool obs_frontend_virtualcam_active(void) override + { + return os_atomic_load_bool(&virtualcam_active); + } + + void obs_frontend_reset_video(void) override { main->ResetVideo(); } + + void obs_frontend_open_source_properties(obs_source_t *source) override + { + QMetaObject::invokeMethod(main, "OpenProperties", + Q_ARG(OBSSource, OBSSource(source))); + } + + void obs_frontend_open_source_filters(obs_source_t *source) override + { + QMetaObject::invokeMethod(main, "OpenFilters", + Q_ARG(OBSSource, OBSSource(source))); + } + + void obs_frontend_open_source_interaction(obs_source_t *source) override + { + QMetaObject::invokeMethod(main, "OpenInteraction", + Q_ARG(OBSSource, OBSSource(source))); + } + + void obs_frontend_open_sceneitem_edit_transform( + obs_sceneitem_t *item) override + { + QMetaObject::invokeMethod(main, "OpenEditTransform", + Q_ARG(OBSSceneItem, + OBSSceneItem(item))); + } + + char *obs_frontend_get_current_record_output_path(void) override + { + const char *recordOutputPath = main->GetCurrentOutputPath(); + + return bstrdup(recordOutputPath); + } + + const char *obs_frontend_get_locale_string(const char *string) override + { + return Str(string); + } + + bool obs_frontend_is_theme_dark(void) override + { + return App()->IsThemeDark(); + } + + char *obs_frontend_get_last_recording(void) override + { + return bstrdup(main->outputHandler->lastRecordingPath.c_str()); + } + + char *obs_frontend_get_last_screenshot(void) override + { + return bstrdup(main->lastScreenshot.c_str()); + } + + char *obs_frontend_get_last_replay(void) override + { + return bstrdup(main->lastReplay.c_str()); + } + + void obs_frontend_add_undo_redo_action(const char *name, + const undo_redo_cb undo, + const undo_redo_cb redo, + const char *undo_data, + const char *redo_data, + bool repeatable) override + { + main->undo_s.add_action( + name, + [undo](const std::string &data) { undo(data.c_str()); }, + [redo](const std::string &data) { redo(data.c_str()); }, + undo_data, redo_data, repeatable); + } + + void on_load(obs_data_t *settings) override + { + for (size_t i = saveCallbacks.size(); i > 0; i--) { + auto cb = saveCallbacks[i - 1]; + cb.callback(settings, false, cb.private_data); + } + } + + void on_preload(obs_data_t *settings) override + { + for (size_t i = preloadCallbacks.size(); i > 0; i--) { + auto cb = preloadCallbacks[i - 1]; + cb.callback(settings, false, cb.private_data); + } + } + + void on_save(obs_data_t *settings) override + { + for (size_t i = saveCallbacks.size(); i > 0; i--) { + auto cb = saveCallbacks[i - 1]; + cb.callback(settings, true, cb.private_data); + } + } + + void on_event(enum obs_frontend_event event) override + { + if (main->disableSaving && + event != OBS_FRONTEND_EVENT_SCENE_COLLECTION_CLEANUP && + event != OBS_FRONTEND_EVENT_EXIT) + return; + + for (size_t i = callbacks.size(); i > 0; i--) { + auto cb = callbacks[i - 1]; + cb.callback(event, cb.private_data); + } + } +}; + +obs_frontend_callbacks *InitializeAPIInterface(OBSBasic *main) +{ + obs_frontend_callbacks *api = new OBSStudioAPI(main); + obs_frontend_set_callbacks_internal(api); + return api; +} diff --git a/UI/obs-app.cpp b/UI/obs-app.cpp index 3417f1553..f4b9bf509 100644 --- a/UI/obs-app.cpp +++ b/UI/obs-app.cpp @@ -609,111 +609,43 @@ static bool MakeUserDirs() return true; } +constexpr std::string_view OBSProfileSubDirectory = "obs-studio/basic/profiles"; +constexpr std::string_view OBSScenesSubDirectory = "obs-studio/basic/scenes"; + static bool MakeUserProfileDirs() { - char path[512]; + const std::filesystem::path userProfilePath = + App()->userProfilesLocation / + std::filesystem::u8path(OBSProfileSubDirectory); + const std::filesystem::path userScenesPath = + App()->userScenesLocation / + std::filesystem::u8path(OBSScenesSubDirectory); - if (GetConfigPath(path, sizeof(path), "obs-studio/basic/profiles") <= 0) - return false; - if (!do_mkdir(path)) - return false; + if (!std::filesystem::exists(userProfilePath)) { + try { + std::filesystem::create_directories(userProfilePath); + } catch (const std::filesystem::filesystem_error &error) { + blog(LOG_ERROR, + "Failed to create user profile directory '%s'\n%s", + userProfilePath.u8string().c_str(), error.what()); + return false; + } + } - if (GetConfigPath(path, sizeof(path), "obs-studio/basic/scenes") <= 0) - return false; - if (!do_mkdir(path)) - return false; + if (!std::filesystem::exists(userScenesPath)) { + try { + std::filesystem::create_directories(userScenesPath); + } catch (const std::filesystem::filesystem_error &error) { + blog(LOG_ERROR, + "Failed to create user scene collection directory '%s'\n%s", + userScenesPath.u8string().c_str(), error.what()); + return false; + } + } return true; } -static string GetProfileDirFromName(const char *name) -{ - string outputPath; - os_glob_t *glob; - char path[512]; - - if (GetConfigPath(path, sizeof(path), "obs-studio/basic/profiles") <= 0) - return outputPath; - - strcat(path, "/*"); - - if (os_glob(path, 0, &glob) != 0) - return outputPath; - - for (size_t i = 0; i < glob->gl_pathc; i++) { - struct os_globent ent = glob->gl_pathv[i]; - if (!ent.directory) - continue; - - strcpy(path, ent.path); - strcat(path, "/basic.ini"); - - ConfigFile config; - if (config.Open(path, CONFIG_OPEN_EXISTING) != 0) - continue; - - const char *curName = - config_get_string(config, "General", "Name"); - if (astrcmpi(curName, name) == 0) { - outputPath = ent.path; - break; - } - } - - os_globfree(glob); - - if (!outputPath.empty()) { - replace(outputPath.begin(), outputPath.end(), '\\', '/'); - const char *start = strrchr(outputPath.c_str(), '/'); - if (start) - outputPath.erase(0, start - outputPath.c_str() + 1); - } - - return outputPath; -} - -static string GetSceneCollectionFileFromName(const char *name) -{ - string outputPath; - os_glob_t *glob; - char path[512]; - - if (GetConfigPath(path, sizeof(path), "obs-studio/basic/scenes") <= 0) - return outputPath; - - strcat(path, "/*.json"); - - if (os_glob(path, 0, &glob) != 0) - return outputPath; - - for (size_t i = 0; i < glob->gl_pathc; i++) { - struct os_globent ent = glob->gl_pathv[i]; - if (ent.directory) - continue; - - OBSDataAutoRelease data = - obs_data_create_from_json_file_safe(ent.path, "bak"); - const char *curName = obs_data_get_string(data, "name"); - - if (astrcmpi(name, curName) == 0) { - outputPath = ent.path; - break; - } - } - - os_globfree(glob); - - if (!outputPath.empty()) { - outputPath.resize(outputPath.size() - 5); - replace(outputPath.begin(), outputPath.end(), '\\', '/'); - const char *start = strrchr(outputPath.c_str(), '/'); - if (start) - outputPath.erase(0, start - outputPath.c_str() + 1); - } - - return outputPath; -} - bool OBSApp::UpdatePre22MultiviewLayout(const char *layout) { if (!layout) @@ -1212,56 +1144,77 @@ OBSApp::~OBSApp() static void move_basic_to_profiles(void) { char path[512]; - char new_path[512]; - os_glob_t *glob; - /* if not first time use */ - if (GetConfigPath(path, 512, "obs-studio/basic") <= 0) + if (GetAppConfigPath(path, 512, "obs-studio/basic") <= 0) { return; - if (!os_file_exists(path)) - return; - - /* if the profiles directory doesn't already exist */ - if (GetConfigPath(new_path, 512, "obs-studio/basic/profiles") <= 0) - return; - if (os_file_exists(new_path)) - return; - - if (os_mkdir(new_path) == MKDIR_ERROR) - return; - - strcat(new_path, "/"); - strcat(new_path, Str("Untitled")); - if (os_mkdir(new_path) == MKDIR_ERROR) - return; - - strcat(path, "/*.*"); - if (os_glob(path, 0, &glob) != 0) - return; - - strcpy(path, new_path); - - for (size_t i = 0; i < glob->gl_pathc; i++) { - struct os_globent ent = glob->gl_pathv[i]; - char *file; - - if (ent.directory) - continue; - - file = strrchr(ent.path, '/'); - if (!file++) - continue; - - if (astrcmpi(file, "scenes.json") == 0) - continue; - - strcpy(new_path, path); - strcat(new_path, "/"); - strcat(new_path, file); - os_rename(ent.path, new_path); } - os_globfree(glob); + const std::filesystem::path basicPath = std::filesystem::u8path(path); + + if (!std::filesystem::exists(basicPath)) { + return; + } + + const std::filesystem::path profilesPath = + App()->userProfilesLocation / + std::filesystem::u8path("obs-studio/basic/profiles"); + + if (std::filesystem::exists(profilesPath)) { + return; + } + + try { + std::filesystem::create_directories(profilesPath); + } catch (const std::filesystem::filesystem_error &error) { + blog(LOG_ERROR, + "Failed to create profiles directory for migration from basic profile\n%s", + error.what()); + return; + } + + const std::filesystem::path newProfilePath = + profilesPath / std::filesystem::u8path(Str("Untitled")); + + for (auto &entry : std::filesystem::directory_iterator(basicPath)) { + if (entry.is_directory()) { + continue; + } + + if (entry.path().filename().u8string() == "scenes.json") { + continue; + } + + if (!std::filesystem::exists(newProfilePath)) { + try { + std::filesystem::create_directory( + newProfilePath); + } catch ( + const std::filesystem::filesystem_error &error) { + blog(LOG_ERROR, + "Failed to create profile directory for 'Untitled'\n%s", + error.what()); + return; + } + } + + const filesystem::path destinationFile = + newProfilePath / entry.path().filename(); + + const auto copyOptions = + std::filesystem::copy_options::overwrite_existing; + + try { + std::filesystem::copy(entry.path(), destinationFile, + copyOptions); + } catch (const std::filesystem::filesystem_error &error) { + blog(LOG_ERROR, + "Failed to copy basic profile file '%s' to new profile 'Untitled'\n%s", + entry.path().filename().u8string().c_str(), + error.what()); + + return; + } + } } static void move_basic_to_scene_collections(void) diff --git a/UI/window-basic-auto-config.cpp b/UI/window-basic-auto-config.cpp index 82907841d..1133cddd6 100644 --- a/UI/window-basic-auto-config.cpp +++ b/UI/window-basic-auto-config.cpp @@ -39,18 +39,24 @@ extern QCefCookieManager *panel_cookies; /* ------------------------------------------------------------------------- */ -#define SERVICE_PATH "service.json" +constexpr std::string_view OBSServiceFileName = "service.json"; static OBSData OpenServiceSettings(std::string &type) { - char serviceJsonPath[512]; - int ret = GetProfilePath(serviceJsonPath, sizeof(serviceJsonPath), - SERVICE_PATH); - if (ret <= 0) - return OBSData(); + const OBSBasic *basic = + reinterpret_cast(App()->GetMainWindow()); + const OBSProfile ¤tProfile = basic->GetCurrentProfile(); - OBSDataAutoRelease data = - obs_data_create_from_json_file_safe(serviceJsonPath, "bak"); + const std::filesystem::path jsonFilePath = + currentProfile.path / + std::filesystem::u8path(OBSServiceFileName); + + if (!std::filesystem::exists(jsonFilePath)) { + return OBSData(); + } + + OBSDataAutoRelease data = obs_data_create_from_json_file_safe( + jsonFilePath.u8string().c_str(), "bak"); obs_data_set_default_string(data, "type", "rtmp_common"); type = obs_data_get_string(data, "type"); diff --git a/UI/window-basic-main-outputs.cpp b/UI/window-basic-main-outputs.cpp index 93ab1d7e3..9177f1084 100644 --- a/UI/window-basic-main-outputs.cpp +++ b/UI/window-basic-main-outputs.cpp @@ -1616,19 +1616,28 @@ struct AdvancedOutput : BasicOutputHandler { static OBSData GetDataFromJsonFile(const char *jsonFile) { - char fullPath[512]; + const OBSBasic *basic = + reinterpret_cast(App()->GetMainWindow()); + + const OBSProfile ¤tProfile = basic->GetCurrentProfile(); + + const std::filesystem::path jsonFilePath = + currentProfile.path / std::filesystem::u8path(jsonFile); + OBSDataAutoRelease data = nullptr; - int ret = GetProfilePath(fullPath, sizeof(fullPath), jsonFile); - if (ret > 0) { - BPtr jsonData = os_quick_read_utf8_file(fullPath); + if (!jsonFilePath.empty()) { + BPtr jsonData = os_quick_read_utf8_file( + jsonFilePath.u8string().c_str()); + if (!!jsonData) { data = obs_data_create_from_json(jsonData); } } - if (!data) + if (!data) { data = obs_data_create(); + } return data.Get(); } diff --git a/UI/window-basic-main-profiles.cpp b/UI/window-basic-main-profiles.cpp index 7492e51d7..3a54e5422 100644 --- a/UI/window-basic-main-profiles.cpp +++ b/UI/window-basic-main-profiles.cpp @@ -15,6 +15,11 @@ along with this program. If not, see . ******************************************************************************/ +#include +#include +#include +#include +#include #include #include #include @@ -26,467 +31,773 @@ #include "window-basic-auto-config.hpp" #include "window-namedialog.hpp" +// MARK: Constant Expressions + +constexpr std::string_view OBSProfilePath = "/obs-studio/basic/profiles/"; +constexpr std::string_view OBSProfileSettingsFile = "basic.ini"; + +// MARK: Forward Declarations + extern void DestroyPanelCookieManager(); extern void DuplicateCurrentCookieProfile(ConfigFile &config); extern void CheckExistingCookieId(); extern void DeleteCookies(); -void EnumProfiles(std::function &&cb) +// MARK: - Main Profile Management Functions + +void OBSBasic::SetupNewProfile(const std::string &profileName, bool useWizard) { - char path[512]; - os_glob_t *glob; + const OBSProfile &newProfile = CreateProfile(profileName); - int ret = GetConfigPath(path, sizeof(path), - "obs-studio/basic/profiles/*"); - if (ret <= 0) { - blog(LOG_WARNING, "Failed to get profiles config path"); - return; - } + config_set_bool(App()->GetUserConfig(), "Basic", "ConfigOnNewProfile", + useWizard); - if (os_glob(path, 0, &glob) != 0) { - blog(LOG_WARNING, "Failed to glob profiles"); - return; - } + ActivateProfile(newProfile, true); - for (size_t i = 0; i < glob->gl_pathc; i++) { - const char *filePath = glob->gl_pathv[i].path; - const char *dirName = strrchr(filePath, '/') + 1; - - if (!glob->gl_pathv[i].directory) - continue; - - if (strcmp(dirName, ".") == 0 || strcmp(dirName, "..") == 0) - continue; - - std::string file = filePath; - file += "/basic.ini"; - - ConfigFile config; - int ret = config.Open(file.c_str(), CONFIG_OPEN_EXISTING); - if (ret != CONFIG_SUCCESS) - continue; - - const char *name = config_get_string(config, "General", "Name"); - if (!name) - name = strrchr(filePath, '/') + 1; - - if (!cb(name, filePath)) - break; - } - - os_globfree(glob); -} - -static bool GetProfileDir(const char *findName, const char *&profileDir) -{ - bool found = false; - auto func = [&](const char *name, const char *path) { - if (strcmp(name, findName) == 0) { - found = true; - profileDir = strrchr(path, '/') + 1; - return false; - } - return true; - }; - - EnumProfiles(func); - return found; -} - -static bool ProfileExists(const char *findName) -{ - const char *profileDir = nullptr; - return GetProfileDir(findName, profileDir); -} - -static bool AskForProfileName(QWidget *parent, std::string &name, - const char *title, const char *text, - const bool showWizard, bool &wizardChecked, - const char *oldName = nullptr) -{ - for (;;) { - bool success = false; - - if (showWizard) { - success = NameDialog::AskForNameWithOption( - parent, title, text, name, - QTStr("AddProfile.WizardCheckbox"), - wizardChecked, QT_UTF8(oldName)); - } else { - success = NameDialog::AskForName( - parent, title, text, name, QT_UTF8(oldName)); - } - - if (!success) { - return false; - } - if (name.empty()) { - OBSMessageBox::warning(parent, - QTStr("NoNameEntered.Title"), - QTStr("NoNameEntered.Text")); - continue; - } - if (ProfileExists(name.c_str())) { - OBSMessageBox::warning(parent, - QTStr("NameExists.Title"), - QTStr("NameExists.Text")); - continue; - } - break; - } - return true; -} - -static bool FindSafeProfileDirName(const std::string &profileName, - std::string &dirName) -{ - char path[512]; - int ret; - - if (ProfileExists(profileName.c_str())) { - blog(LOG_WARNING, "Profile '%s' exists", profileName.c_str()); - return false; - } - - if (!GetFileSafeName(profileName.c_str(), dirName)) { - blog(LOG_WARNING, "Failed to create safe file name for '%s'", - profileName.c_str()); - return false; - } - - ret = GetConfigPath(path, sizeof(path), "obs-studio/basic/profiles/"); - if (ret <= 0) { - blog(LOG_WARNING, "Failed to get profiles config path"); - return false; - } - - dirName.insert(0, path); - - if (!GetClosestUnusedFileName(dirName, nullptr)) { - blog(LOG_WARNING, "Failed to get closest file name for %s", - dirName.c_str()); - return false; - } - - dirName.erase(0, ret); - return true; -} - -static bool CopyProfile(const char *fromPartial, const char *to) -{ - os_glob_t *glob; - char path[514]; - char dir[512]; - int ret; - - ret = GetConfigPath(dir, sizeof(dir), "obs-studio/basic/profiles/"); - if (ret <= 0) { - blog(LOG_WARNING, "Failed to get profiles config path"); - return false; - } - - snprintf(path, sizeof(path), "%s%s/*", dir, fromPartial); - - if (os_glob(path, 0, &glob) != 0) { - blog(LOG_WARNING, "Failed to glob profile '%s'", fromPartial); - return false; - } - - for (size_t i = 0; i < glob->gl_pathc; i++) { - const char *filePath = glob->gl_pathv[i].path; - if (glob->gl_pathv[i].directory) - continue; - - ret = snprintf(path, sizeof(path), "%s/%s", to, - strrchr(filePath, '/') + 1); - if (ret > 0) { - if (os_copyfile(filePath, path) != 0) { - blog(LOG_WARNING, - "CopyProfile: Failed to " - "copy file %s to %s", - filePath, path); - } - } - } - - os_globfree(glob); - - return true; -} - -static bool ProfileNeedsRestart(config_t *newConfig, QString &settings) -{ - OBSBasic *main = OBSBasic::Get(); - - const char *oldSpeakers = - config_get_string(main->Config(), "Audio", "ChannelSetup"); - uint oldSampleRate = - config_get_uint(main->Config(), "Audio", "SampleRate"); - - const char *newSpeakers = - config_get_string(newConfig, "Audio", "ChannelSetup"); - uint newSampleRate = config_get_uint(newConfig, "Audio", "SampleRate"); - - auto appendSetting = [&settings](const char *name) { - settings += QStringLiteral("\n") + QTStr(name); - }; - - bool result = false; - if (oldSpeakers != NULL && newSpeakers != NULL) { - result = strcmp(oldSpeakers, newSpeakers) != 0; - appendSetting("Basic.Settings.Audio.Channels"); - } - if (oldSampleRate != 0 && newSampleRate != 0) { - result |= oldSampleRate != newSampleRate; - appendSetting("Basic.Settings.Audio.SampleRate"); - } - - return result; -} - -bool OBSBasic::AddProfile(bool create_new, const char *title, const char *text, - const char *init_text, bool rename) -{ - std::string name; - - bool showWizardChecked = config_get_bool(App()->GlobalConfig(), "Basic", - "ConfigOnNewProfile"); - - if (!AskForProfileName(this, name, title, text, create_new, - showWizardChecked, init_text)) - return false; - - return CreateProfile(name, create_new, showWizardChecked, rename); -} - -bool OBSBasic::CreateProfile(const std::string &newName, bool create_new, - bool showWizardChecked, bool rename) -{ - std::string newDir; - std::string newPath; - ConfigFile config; - - if (!FindSafeProfileDirName(newName, newDir)) - return false; - - if (create_new) { - config_set_bool(App()->GlobalConfig(), "Basic", - "ConfigOnNewProfile", showWizardChecked); - } - - std::string curDir = - config_get_string(App()->GlobalConfig(), "Basic", "ProfileDir"); - - char baseDir[512]; - int ret = GetConfigPath(baseDir, sizeof(baseDir), - "obs-studio/basic/profiles/"); - if (ret <= 0) { - blog(LOG_WARNING, "Failed to get profiles config path"); - return false; - } - - newPath = baseDir; - newPath += newDir; - - if (os_mkdir(newPath.c_str()) < 0) { - blog(LOG_WARNING, "Failed to create profile directory '%s'", - newDir.c_str()); - return false; - } - - if (!create_new) - CopyProfile(curDir.c_str(), newPath.c_str()); - - newPath += "/basic.ini"; - - if (config.Open(newPath.c_str(), CONFIG_OPEN_ALWAYS) != 0) { - blog(LOG_ERROR, "Failed to open new config file '%s'", - newDir.c_str()); - return false; - } - - if (!rename) - OnEvent(OBS_FRONTEND_EVENT_PROFILE_CHANGING); - - config_set_string(App()->GlobalConfig(), "Basic", "Profile", - newName.c_str()); - config_set_string(App()->GlobalConfig(), "Basic", "ProfileDir", - newDir.c_str()); - - Auth::Save(); - if (create_new) { - auth.reset(); - DestroyPanelCookieManager(); -#ifdef YOUTUBE_ENABLED - if (youtubeAppDock) - DeleteYouTubeAppDock(); -#endif - } else if (!rename) { - DuplicateCurrentCookieProfile(config); - } - - config_set_string(config, "General", "Name", newName.c_str()); - basicConfig.SaveSafe("tmp"); - config.SaveSafe("tmp"); - config.Swap(basicConfig); - InitBasicConfigDefaults(); - InitBasicConfigDefaults2(); - RefreshProfiles(); - - if (create_new) - ResetProfileData(); - - blog(LOG_INFO, "Created profile '%s' (%s, %s)", newName.c_str(), - create_new ? "clean" : "duplicate", newDir.c_str()); + blog(LOG_INFO, "Created profile '%s' (clean, %s)", + newProfile.name.c_str(), newProfile.directoryName.c_str()); blog(LOG_INFO, "------------------------------------------------"); - config_save_safe(App()->GlobalConfig(), "tmp", nullptr); - UpdateTitleBar(); - UpdateVolumeControlsDecayRate(); - - Auth::Load(); - - // Run auto configuration setup wizard when a new profile is made to assist - // setting up blank settings - if (create_new && showWizardChecked) { + if (useWizard) { AutoConfig wizard(this); wizard.setModal(true); wizard.show(); wizard.exec(); } +} - if (!rename) { - OnEvent(OBS_FRONTEND_EVENT_PROFILE_LIST_CHANGED); - OnEvent(OBS_FRONTEND_EVENT_PROFILE_CHANGED); +void OBSBasic::SetupDuplicateProfile(const std::string &profileName) +{ + const OBSProfile &newProfile = CreateProfile(profileName); + const OBSProfile ¤tProfile = GetCurrentProfile(); + + const auto copyOptions = + std::filesystem::copy_options::recursive | + std::filesystem::copy_options::overwrite_existing; + + try { + std::filesystem::copy(currentProfile.path, newProfile.path, + copyOptions); + } catch (const std::filesystem::filesystem_error &error) { + blog(LOG_DEBUG, "%s", error.what()); + throw std::logic_error( + "Failed to copy files for cloned profile: " + + newProfile.name); } - return true; + + ActivateProfile(newProfile); + + blog(LOG_INFO, "Created profile '%s' (duplicate, %s)", + newProfile.name.c_str(), newProfile.directoryName.c_str()); + blog(LOG_INFO, "------------------------------------------------"); } -bool OBSBasic::NewProfile(const QString &name) +void OBSBasic::SetupRenameProfile(const std::string &profileName) { - return CreateProfile(name.toStdString(), true, false, false); + const OBSProfile &newProfile = CreateProfile(profileName); + const OBSProfile currentProfile = GetCurrentProfile(); + + const auto copyOptions = + std::filesystem::copy_options::recursive | + std::filesystem::copy_options::overwrite_existing; + + try { + std::filesystem::copy(currentProfile.path, newProfile.path, + copyOptions); + } catch (const std::filesystem::filesystem_error &error) { + blog(LOG_DEBUG, "%s", error.what()); + throw std::logic_error("Failed to copy files for profile: " + + currentProfile.name); + } + + profiles.erase(currentProfile.name); + + ActivateProfile(newProfile); + RemoveProfile(currentProfile); + + blog(LOG_INFO, "Renamed profile '%s' to '%s' (%s)", + currentProfile.name.c_str(), newProfile.name.c_str(), + newProfile.directoryName.c_str()); + blog(LOG_INFO, "------------------------------------------------"); + + OnEvent(OBS_FRONTEND_EVENT_PROFILE_RENAMED); } -bool OBSBasic::DuplicateProfile(const QString &name) +// MARK: - Profile File Management Functions + +const OBSProfile &OBSBasic::CreateProfile(const std::string &profileName) { - return CreateProfile(name.toStdString(), false, false, false); + if (const auto &foundProfile = GetProfileByName(profileName)) { + throw std::invalid_argument("Profile already exists: " + + profileName); + } + + std::string directoryName; + if (!GetFileSafeName(profileName.c_str(), directoryName)) { + throw std::invalid_argument( + "Failed to create safe directory for new profile: " + + profileName); + } + + std::string profileDirectory; + profileDirectory.reserve(App()->userProfilesLocation.u8string().size() + + OBSProfilePath.size() + directoryName.size()); + profileDirectory.append(App()->userProfilesLocation.u8string()) + .append(OBSProfilePath) + .append(directoryName); + + if (!GetClosestUnusedFileName(profileDirectory, nullptr)) { + throw std::invalid_argument( + "Failed to get closest directory name for new profile: " + + profileName); + } + + const std::filesystem::path profileDirectoryPath = + std::filesystem::u8path(profileDirectory); + + try { + std::filesystem::create_directory(profileDirectoryPath); + } catch (const std::filesystem::filesystem_error error) { + throw std::logic_error( + "Failed to create directory for new profile: " + + profileDirectory); + } + + const std::filesystem::path profileFile = + profileDirectoryPath / + std::filesystem::u8path(OBSProfileSettingsFile); + + auto [iterator, success] = profiles.try_emplace( + profileName, + OBSProfile{profileName, + profileDirectoryPath.filename().u8string(), + profileDirectoryPath, profileFile}); + + OnEvent(OBS_FRONTEND_EVENT_PROFILE_LIST_CHANGED); + + return iterator->second; } -void OBSBasic::DeleteProfile(const char *profileName, const char *profileDir) +void OBSBasic::RemoveProfile(OBSProfile profile) { - char profilePath[512]; - char basePath[512]; + try { + std::filesystem::remove_all(profile.path); + } catch (const std::filesystem::filesystem_error &error) { + blog(LOG_DEBUG, "%s", error.what()); + throw std::logic_error("Failed to remove profile directory: " + + profile.directoryName); + } - int ret = GetConfigPath(basePath, sizeof(basePath), - "obs-studio/basic/profiles"); - if (ret <= 0) { + blog(LOG_INFO, "------------------------------------------------"); + blog(LOG_INFO, "Removed profile '%s' (%s)", profile.name.c_str(), + profile.directoryName.c_str()); + blog(LOG_INFO, "------------------------------------------------"); +} + +// MARK: - Profile UI Handling Functions + +bool OBSBasic::CreateNewProfile(const QString &name) +{ + try { + SetupNewProfile(name.toStdString()); + return true; + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + return false; + } catch (const std::logic_error &error) { + blog(LOG_ERROR, "%s", error.what()); + return false; + } +} + +bool OBSBasic::CreateDuplicateProfile(const QString &name) +{ + try { + SetupDuplicateProfile(name.toStdString()); + return true; + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + return false; + } catch (const std::logic_error &error) { + blog(LOG_ERROR, "%s", error.what()); + return false; + } +} + +void OBSBasic::DeleteProfile(const QString &name) +{ + const std::string_view currentProfileName{ + config_get_string(App()->GetUserConfig(), "Basic", "Profile")}; + + if (currentProfileName == name.toStdString()) { + on_actionRemoveProfile_triggered(); + return; + } + + auto foundProfile = GetProfileByName(name.toStdString()); + + if (!foundProfile) { + blog(LOG_ERROR, "Invalid profile name: %s", QT_TO_UTF8(name)); + return; + } + + RemoveProfile(foundProfile.value()); + profiles.erase(name.toStdString()); + + RefreshProfiles(); + + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + + OnEvent(OBS_FRONTEND_EVENT_PROFILE_LIST_CHANGED); +} + +void OBSBasic::ChangeProfile() +{ + QAction *action = reinterpret_cast(sender()); + + if (!action) { + return; + } + + const std::string_view currentProfileName{ + config_get_string(App()->GetUserConfig(), "Basic", "Profile")}; + const std::string selectedProfileName{action->text().toStdString()}; + + if (currentProfileName == selectedProfileName) { + action->setChecked(true); + return; + } + + const std::optional foundProfile = + GetProfileByName(selectedProfileName); + + if (!foundProfile) { + const std::string errorMessage{"Selected profile not found: "}; + + throw std::invalid_argument(errorMessage + + currentProfileName.data()); + } + + const OBSProfile &selectedProfile = foundProfile.value(); + + OnEvent(OBS_FRONTEND_EVENT_PROFILE_CHANGING); + + ActivateProfile(selectedProfile, true); + + blog(LOG_INFO, "Switched to profile '%s' (%s)", + selectedProfile.name.c_str(), + selectedProfile.directoryName.c_str()); + blog(LOG_INFO, "------------------------------------------------"); +} + +void OBSBasic::RefreshProfiles(bool refreshCache) +{ + std::string_view currentProfileName{ + config_get_string(App()->GetUserConfig(), "Basic", "Profile")}; + + QList menuActions = ui->profileMenu->actions(); + + for (auto &action : menuActions) { + QVariant variant = action->property("file_name"); + + if (variant.typeName() != nullptr) { + delete action; + } + } + + if (refreshCache) { + RefreshProfileCache(); + } + + size_t numAddedProfiles = 0; + for (auto &[profileName, profile] : profiles) { + QAction *action = + new QAction(QString().fromStdString(profileName), this); + action->setProperty( + "file_name", + QString().fromStdString(profile.directoryName)); + connect(action, &QAction::triggered, this, + &OBSBasic::ChangeProfile); + action->setCheckable(true); + action->setChecked(profileName == currentProfileName); + + ui->profileMenu->addAction(action); + + numAddedProfiles += 1; + } + + ui->actionRemoveProfile->setEnabled(numAddedProfiles > 1); +} + +// MARK: - Profile Cache Functions + +/// Refreshes profile cache data with profile state found on local file system. +void OBSBasic::RefreshProfileCache() +{ + std::map foundProfiles{}; + + const std::filesystem::path profilesPath = + App()->userProfilesLocation / + std::filesystem::u8path(OBSProfilePath.substr(1)); + + if (!std::filesystem::exists(profilesPath)) { blog(LOG_WARNING, "Failed to get profiles config path"); return; } - ret = snprintf(profilePath, sizeof(profilePath), "%s/%s/*", basePath, - profileDir); - if (ret <= 0) { - blog(LOG_WARNING, "Failed to get path for profile dir '%s'", - profileDir); - return; - } - - os_glob_t *glob; - if (os_glob(profilePath, 0, &glob) != 0) { - blog(LOG_WARNING, "Failed to glob profile dir '%s'", - profileDir); - return; - } - - for (size_t i = 0; i < glob->gl_pathc; i++) { - const char *filePath = glob->gl_pathv[i].path; - - if (glob->gl_pathv[i].directory) + for (const auto &entry : + std::filesystem::directory_iterator(profilesPath)) { + if (!entry.is_directory()) { continue; + } - os_unlink(filePath); + std::string profileCandidate; + profileCandidate.reserve(entry.path().u8string().size() + + OBSProfileSettingsFile.size() + 1); + profileCandidate.append(entry.path().u8string()) + .append("/") + .append(OBSProfileSettingsFile); + + ConfigFile config; + + if (config.Open(profileCandidate.c_str(), + CONFIG_OPEN_EXISTING) != CONFIG_SUCCESS) { + continue; + } + + std::string candidateName; + const char *configName = + config_get_string(config, "General", "Name"); + + if (configName) { + candidateName = configName; + } else { + candidateName = entry.path().filename().u8string(); + } + + foundProfiles.try_emplace( + candidateName, + OBSProfile{candidateName, + entry.path().filename().u8string(), + entry.path(), profileCandidate}); } - os_globfree(glob); + profiles.swap(foundProfiles); +} - ret = snprintf(profilePath, sizeof(profilePath), "%s/%s", basePath, - profileDir); - if (ret <= 0) { - blog(LOG_WARNING, "Failed to get path for profile dir '%s'", - profileDir); +const OBSProfile &OBSBasic::GetCurrentProfile() const +{ + std::string currentProfileName{ + config_get_string(App()->GetUserConfig(), "Basic", "Profile")}; + + if (currentProfileName.empty()) { + throw std::invalid_argument( + "No valid profile name in configuration Basic->Profile"); + } + + const auto &foundProfile = profiles.find(currentProfileName); + + if (foundProfile != profiles.end()) { + return foundProfile->second; + } else { + throw std::invalid_argument( + "Profile not found in profile list: " + + currentProfileName); + } +} + +std::optional +OBSBasic::GetProfileByName(const std::string &profileName) const +{ + auto foundProfile = profiles.find(profileName); + + if (foundProfile == profiles.end()) { + return {}; + } else { + return foundProfile->second; + } +} + +std::optional +OBSBasic::GetProfileByDirectoryName(const std::string &directoryName) const +{ + for (auto &[iterator, profile] : profiles) { + if (profile.directoryName == directoryName) { + return profile; + } + } + + return {}; +} + +// MARK: - Qt Slot Functions + +void OBSBasic::on_actionNewProfile_triggered() +{ + bool useProfileWizard = config_get_bool(App()->GetUserConfig(), "Basic", + "ConfigOnNewProfile"); + + const OBSPromptCallback profilePromptCallback = + [this](const OBSPromptResult &result) { + if (GetProfileByName(result.promptValue)) { + return false; + } + + return true; + }; + + const OBSPromptRequest request{Str("AddProfile.Title"), + Str("AddProfile.Text"), + "", + true, + Str("AddProfile.WizardCheckbox"), + useProfileWizard}; + + OBSPromptResult result = PromptForName(request, profilePromptCallback); + + if (!result.success) { return; } - os_rmdir(profilePath); + try { + SetupNewProfile(result.promptValue, result.optionValue); + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + } catch (const std::logic_error &error) { + blog(LOG_ERROR, "%s", error.what()); + } +} - blog(LOG_INFO, "------------------------------------------------"); - blog(LOG_INFO, "Removed profile '%s' (%s)", profileName, profileDir); +void OBSBasic::on_actionDupProfile_triggered() +{ + const OBSPromptCallback profilePromptCallback = + [this](const OBSPromptResult &result) { + if (GetProfileByName(result.promptValue)) { + return false; + } + + return true; + }; + + const OBSPromptRequest request{Str("AddProfile.Title"), + Str("AddProfile.Text")}; + + OBSPromptResult result = PromptForName(request, profilePromptCallback); + + if (!result.success) { + return; + } + + try { + SetupDuplicateProfile(result.promptValue); + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + } catch (const std::logic_error &error) { + blog(LOG_ERROR, "%s", error.what()); + } +} + +void OBSBasic::on_actionRenameProfile_triggered() +{ + const std::string currentProfileName = + config_get_string(App()->GetUserConfig(), "Basic", "Profile"); + + const OBSPromptCallback profilePromptCallback = + [this](const OBSPromptResult &result) { + if (GetProfileByName(result.promptValue)) { + return false; + } + + return true; + }; + + const OBSPromptRequest request{Str("RenameProfile.Title"), + Str("RenameProfile.Text"), + currentProfileName}; + + OBSPromptResult result = PromptForName(request, profilePromptCallback); + + if (!result.success) { + return; + } + + try { + SetupRenameProfile(result.promptValue); + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + } catch (const std::logic_error &error) { + blog(LOG_ERROR, "%s", error.what()); + } +} + +void OBSBasic::on_actionRemoveProfile_triggered(bool skipConfirmation) +{ + if (profiles.size() < 2) { + return; + } + + OBSProfile currentProfile; + + try { + currentProfile = GetCurrentProfile(); + + if (!skipConfirmation) { + const QString confirmationText = + QTStr("ConfirmRemove.Text") + .arg(QString::fromStdString( + currentProfile.name)); + + const QMessageBox::StandardButton button = + OBSMessageBox::question( + this, QTStr("ConfirmRemove.Title"), + confirmationText); + + if (button == QMessageBox::No) { + return; + } + } + + OnEvent(OBS_FRONTEND_EVENT_PROFILE_CHANGING); + + profiles.erase(currentProfile.name); + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + } catch (const std::logic_error &error) { + blog(LOG_ERROR, "%s", error.what()); + } + + const OBSProfile &newProfile = profiles.rbegin()->second; + + ActivateProfile(newProfile, true); + RemoveProfile(currentProfile); + +#ifdef YOUTUBE_ENABLED + if (YouTubeAppDock::IsYTServiceSelected() && !youtubeAppDock) + NewYouTubeAppDock(); +#endif + + blog(LOG_INFO, "Switched to profile '%s' (%s)", newProfile.name.c_str(), + newProfile.directoryName.c_str()); blog(LOG_INFO, "------------------------------------------------"); } -void OBSBasic::DeleteProfile(const QString &profileName) +void OBSBasic::on_actionImportProfile_triggered() { - std::string name = profileName.toStdString(); - const char *curName = - config_get_string(App()->GlobalConfig(), "Basic", "Profile"); + const QString home = QDir::homePath(); - if (strcmp(curName, name.c_str()) == 0) { - on_actionRemoveProfile_triggered(true); - return; + const QString sourceDirectory = SelectDirectory( + this, QTStr("Basic.MainMenu.Profile.Import"), home); + + if (!sourceDirectory.isEmpty() && !sourceDirectory.isNull()) { + const std::filesystem::path sourcePath = + std::filesystem::u8path(sourceDirectory.toStdString()); + const std::string directoryName = + sourcePath.filename().string(); + + if (auto profile = GetProfileByDirectoryName(directoryName)) { + OBSMessageBox::warning( + this, QTStr("Basic.MainMenu.Profile.Import"), + QTStr("Basic.MainMenu.Profile.Exists")); + return; + } + + std::string destinationPathString; + destinationPathString.reserve( + App()->userProfilesLocation.u8string().size() + + OBSProfilePath.size() + directoryName.size()); + destinationPathString + .append(App()->userProfilesLocation.u8string()) + .append(OBSProfilePath) + .append(directoryName); + + const std::filesystem::path destinationPath = + std::filesystem::u8path(destinationPathString); + + try { + std::filesystem::create_directory(destinationPath); + } catch (const std::filesystem::filesystem_error &error) { + blog(LOG_WARNING, + "Failed to create profile directory '%s':\n%s", + directoryName.c_str(), error.what()); + return; + } + + const std::array, 4> profileFiles{{ + {"basic.ini", true}, + {"service.json", false}, + {"streamEncoder.json", false}, + {"recordEncoder.json", false}, + }}; + + for (auto &[file, isMandatory] : profileFiles) { + const std::filesystem::path sourceFile = + sourcePath / std::filesystem::u8path(file); + + if (!std::filesystem::exists(sourceFile)) { + if (isMandatory) { + blog(LOG_ERROR, + "Failed to import profile from directory '%s' - necessary file '%s' not found", + directoryName.c_str(), + file.c_str()); + return; + } + continue; + } + + const std::filesystem::path destinationFile = + destinationPath / std::filesystem::u8path(file); + + try { + std::filesystem::copy(sourceFile, + destinationFile); + } catch ( + const std::filesystem::filesystem_error &error) { + blog(LOG_WARNING, + "Failed to copy import file '%s' for profile '%s':\n%s", + file.c_str(), directoryName.c_str(), + error.what()); + return; + } + } + + RefreshProfiles(true); + } +} + +void OBSBasic::on_actionExportProfile_triggered() +{ + const OBSProfile ¤tProfile = GetCurrentProfile(); + + const QString home = QDir::homePath(); + + const QString destinationDirectory = SelectDirectory( + this, QTStr("Basic.MainMenu.Profile.Export"), home); + + const std::array profileFiles{"basic.ini", + "service.json", + "streamEncoder.json", + "recordEncoder.json"}; + + if (!destinationDirectory.isEmpty() && !destinationDirectory.isNull()) { + const std::filesystem::path sourcePath = currentProfile.path; + const std::filesystem::path destinationPath = + std::filesystem::u8path( + destinationDirectory.toStdString()) / + std::filesystem::u8path(currentProfile.directoryName); + + if (!std::filesystem::exists(destinationPath)) { + std::filesystem::create_directory(destinationPath); + } + + std::filesystem::copy_options copyOptions = + std::filesystem::copy_options::overwrite_existing; + + for (auto &file : profileFiles) { + const std::filesystem::path sourceFile = + sourcePath / std::filesystem::u8path(file); + + if (!std::filesystem::exists(sourceFile)) { + continue; + } + + const std::filesystem::path destinationFile = + destinationPath / std::filesystem::u8path(file); + + try { + std::filesystem::copy(sourceFile, + destinationFile, + copyOptions); + } catch ( + const std::filesystem::filesystem_error &error) { + blog(LOG_WARNING, + "Failed to copy export file '%s' for profile '%s'\n%s", + file.c_str(), currentProfile.name.c_str(), + error.what()); + return; + } + } + } +} + +// MARK: - Profile Management Helper Functions + +void OBSBasic::ActivateProfile(const OBSProfile &profile, bool reset) +{ + ConfigFile config; + if (config.Open(profile.profileFile.u8string().c_str(), + CONFIG_OPEN_ALWAYS) != CONFIG_SUCCESS) { + throw std::logic_error( + "failed to open configuration file of new profile: " + + profile.profileFile.string()); } - const char *profileDir = nullptr; - if (!GetProfileDir(name.c_str(), profileDir)) { - blog(LOG_WARNING, "Profile '%s' not found", name.c_str()); - return; + config_set_string(config, "General", "Name", profile.name.c_str()); + config.SaveSafe("tmp"); + + std::vector restartRequirements; + + if (activeConfiguration) { + Auth::Save(); + + if (reset) { + auth.reset(); + DestroyPanelCookieManager(); +#ifdef YOUTUBE_ENABLED + if (youtubeAppDock) { + DeleteYouTubeAppDock(); + } +#endif + } + restartRequirements = GetRestartRequirements(config); + + activeConfiguration.SaveSafe("tmp"); } - if (!profileDir) { - blog(LOG_WARNING, "Failed to get profile dir for profile '%s'", - name.c_str()); - return; + activeConfiguration.Swap(config); + + config_set_string(App()->GetUserConfig(), "Basic", "Profile", + profile.name.c_str()); + config_set_string(App()->GetUserConfig(), "Basic", "ProfileDir", + profile.directoryName.c_str()); + + config_save_safe(App()->GetUserConfig(), "tmp", nullptr); + + InitBasicConfigDefaults(); + InitBasicConfigDefaults2(); + + if (reset) { + ResetProfileData(); } - DeleteProfile(name.c_str(), profileDir); + CheckForSimpleModeX264Fallback(); + RefreshProfiles(); - config_save_safe(App()->GlobalConfig(), "tmp", nullptr); - OnEvent(OBS_FRONTEND_EVENT_PROFILE_LIST_CHANGED); -} -void OBSBasic::RefreshProfiles() -{ - QList menuActions = ui->profileMenu->actions(); - int count = 0; + UpdateTitleBar(); + UpdateVolumeControlsDecayRate(); - for (int i = 0; i < menuActions.count(); i++) { - QVariant v = menuActions[i]->property("file_name"); - if (v.typeName() != nullptr) - delete menuActions[i]; + Auth::Load(); + + OnEvent(OBS_FRONTEND_EVENT_PROFILE_CHANGED); + + if (!restartRequirements.empty()) { + std::string requirements = std::accumulate( + std::next(restartRequirements.begin()), + restartRequirements.end(), restartRequirements[0], + [](std::string a, std::string b) { + return std::move(a) + "\n" + b; + }); + + QMessageBox::StandardButton button = OBSMessageBox::question( + this, QTStr("Restart"), + QTStr("LoadProfileNeedsRestart") + .arg(requirements.c_str())); + + if (button == QMessageBox::Yes) { + restart = true; + close(); + } } - - const char *curName = - config_get_string(App()->GlobalConfig(), "Basic", "Profile"); - - auto addProfile = [&](const char *name, const char *path) { - std::string file = strrchr(path, '/') + 1; - - QAction *action = new QAction(QT_UTF8(name), this); - action->setProperty("file_name", QT_UTF8(path)); - connect(action, &QAction::triggered, this, - &OBSBasic::ChangeProfile); - action->setCheckable(true); - - action->setChecked(strcmp(name, curName) == 0); - - ui->profileMenu->addAction(action); - count++; - return true; - }; - - EnumProfiles(addProfile); - - ui->actionRemoveProfile->setEnabled(count > 1); } void OBSBasic::ResetProfileData() @@ -501,9 +812,9 @@ void OBSBasic::ResetProfileData() /* load audio monitoring */ if (obs_audio_monitoring_available()) { const char *device_name = config_get_string( - basicConfig, "Audio", "MonitoringDeviceName"); - const char *device_id = config_get_string(basicConfig, "Audio", - "MonitoringDeviceId"); + activeConfiguration, "Audio", "MonitoringDeviceName"); + const char *device_id = config_get_string( + activeConfiguration, "Audio", "MonitoringDeviceId"); obs_set_audio_monitoring_device(device_name, device_id); @@ -512,320 +823,44 @@ void OBSBasic::ResetProfileData() } } -void OBSBasic::on_actionNewProfile_triggered() +std::vector +OBSBasic::GetRestartRequirements(const ConfigFile &config) const { - AddProfile(true, Str("AddProfile.Title"), Str("AddProfile.Text")); -} + std::vector result; -void OBSBasic::on_actionDupProfile_triggered() -{ - AddProfile(false, Str("AddProfile.Title"), Str("AddProfile.Text")); -} + const char *oldSpeakers = + config_get_string(activeConfiguration, "Audio", "ChannelSetup"); + const char *newSpeakers = + config_get_string(config, "Audio", "ChannelSetup"); -void OBSBasic::on_actionRenameProfile_triggered() -{ - std::string curDir = - config_get_string(App()->GlobalConfig(), "Basic", "ProfileDir"); - std::string curName = - config_get_string(App()->GlobalConfig(), "Basic", "Profile"); + uint64_t oldSampleRate = + config_get_uint(activeConfiguration, "Audio", "SampleRate"); + uint64_t newSampleRate = config_get_uint(config, "Audio", "SampleRate"); - /* Duplicate and delete in case there are any issues in the process */ - bool success = AddProfile(false, Str("RenameProfile.Title"), - Str("AddProfile.Text"), curName.c_str(), - true); - if (success) { - DeleteProfile(curName.c_str(), curDir.c_str()); - RefreshProfiles(); - } - - OnEvent(OBS_FRONTEND_EVENT_PROFILE_RENAMED); -} - -void OBSBasic::on_actionRemoveProfile_triggered(bool skipConfirmation) -{ - std::string newName; - std::string newPath; - ConfigFile config; - - std::string oldDir = - config_get_string(App()->GlobalConfig(), "Basic", "ProfileDir"); - std::string oldName = - config_get_string(App()->GlobalConfig(), "Basic", "Profile"); - - auto cb = [&](const char *name, const char *filePath) { - if (strcmp(oldName.c_str(), name) != 0) { - newName = name; - newPath = filePath; - return false; - } - - return true; - }; - - EnumProfiles(cb); - - /* this should never be true due to menu item being grayed out */ - if (newPath.empty()) - return; - - if (!skipConfirmation) { - QString text = QTStr("ConfirmRemove.Text") - .arg(QT_UTF8(oldName.c_str())); - - QMessageBox::StandardButton button = OBSMessageBox::question( - this, QTStr("ConfirmRemove.Title"), text); - if (button == QMessageBox::No) - return; - } - - size_t newPath_len = newPath.size(); - newPath += "/basic.ini"; - - if (config.Open(newPath.c_str(), CONFIG_OPEN_ALWAYS) != 0) { - blog(LOG_ERROR, "ChangeProfile: Failed to load file '%s'", - newPath.c_str()); - return; - } - - OnEvent(OBS_FRONTEND_EVENT_PROFILE_CHANGING); - - newPath.resize(newPath_len); - - const char *newDir = strrchr(newPath.c_str(), '/') + 1; - - config_set_string(App()->GlobalConfig(), "Basic", "Profile", - newName.c_str()); - config_set_string(App()->GlobalConfig(), "Basic", "ProfileDir", newDir); - - QString settingsRequiringRestart; - bool needsRestart = - ProfileNeedsRestart(config, settingsRequiringRestart); - - Auth::Save(); - auth.reset(); - DeleteCookies(); - DestroyPanelCookieManager(); - - config.Swap(basicConfig); - InitBasicConfigDefaults(); - InitBasicConfigDefaults2(); - ResetProfileData(); - DeleteProfile(oldName.c_str(), oldDir.c_str()); - RefreshProfiles(); - config_save_safe(App()->GlobalConfig(), "tmp", nullptr); - - blog(LOG_INFO, "Switched to profile '%s' (%s)", newName.c_str(), - newDir); - blog(LOG_INFO, "------------------------------------------------"); - - UpdateTitleBar(); - UpdateVolumeControlsDecayRate(); - - Auth::Load(); - - OnEvent(OBS_FRONTEND_EVENT_PROFILE_LIST_CHANGED); - OnEvent(OBS_FRONTEND_EVENT_PROFILE_CHANGED); - - if (needsRestart) { - QMessageBox::StandardButton button = OBSMessageBox::question( - this, QTStr("Restart"), - QTStr("LoadProfileNeedsRestart") - .arg(settingsRequiringRestart)); - - if (button == QMessageBox::Yes) { - restart = true; - close(); + if (oldSpeakers != NULL && newSpeakers != NULL) { + if (std::string_view{oldSpeakers} != + std::string_view{newSpeakers}) { + result.emplace_back( + Str("Basic.Settings.Audio.Channels")); } } -} -void OBSBasic::on_actionImportProfile_triggered() -{ - char path[512]; - - QString home = QDir::homePath(); - - int ret = GetConfigPath(path, 512, "obs-studio/basic/profiles/"); - if (ret <= 0) { - blog(LOG_WARNING, "Failed to get profile config path"); - return; - } - - QString dir = SelectDirectory( - this, QTStr("Basic.MainMenu.Profile.Import"), home); - - if (!dir.isEmpty() && !dir.isNull()) { - QString inputPath = QString::fromUtf8(path); - QFileInfo finfo(dir); - QString directory = finfo.fileName(); - QString profileDir = inputPath + directory; - - if (ProfileExists(directory.toStdString().c_str())) { - OBSMessageBox::warning( - this, QTStr("Basic.MainMenu.Profile.Import"), - QTStr("Basic.MainMenu.Profile.Exists")); - } else if (os_mkdir(profileDir.toStdString().c_str()) < 0) { - blog(LOG_WARNING, - "Failed to create profile directory '%s'", - directory.toStdString().c_str()); - } else { - QFile::copy(dir + "/basic.ini", - profileDir + "/basic.ini"); - QFile::copy(dir + "/service.json", - profileDir + "/service.json"); - QFile::copy(dir + "/streamEncoder.json", - profileDir + "/streamEncoder.json"); - QFile::copy(dir + "/recordEncoder.json", - profileDir + "/recordEncoder.json"); - RefreshProfiles(); + if (oldSampleRate != 0 && newSampleRate != 0) { + if (oldSampleRate != newSampleRate) { + result.emplace_back( + Str("Basic.Settings.Audio.SampleRate")); } } -} -void OBSBasic::on_actionExportProfile_triggered() -{ - char path[512]; - - QString home = QDir::homePath(); - - QString currentProfile = QString::fromUtf8(config_get_string( - App()->GlobalConfig(), "Basic", "ProfileDir")); - - int ret = GetConfigPath(path, 512, "obs-studio/basic/profiles/"); - if (ret <= 0) { - blog(LOG_WARNING, "Failed to get profile config path"); - return; - } - - QString dir = SelectDirectory( - this, QTStr("Basic.MainMenu.Profile.Export"), home); - - if (!dir.isEmpty() && !dir.isNull()) { - QString outputDir = dir + "/" + currentProfile; - QString inputPath = QString::fromUtf8(path); - QDir folder(outputDir); - - if (!folder.exists()) { - folder.mkpath(outputDir); - } else { - if (QFile::exists(outputDir + "/basic.ini")) - QFile::remove(outputDir + "/basic.ini"); - - if (QFile::exists(outputDir + "/service.json")) - QFile::remove(outputDir + "/service.json"); - - if (QFile::exists(outputDir + "/streamEncoder.json")) - QFile::remove(outputDir + - "/streamEncoder.json"); - - if (QFile::exists(outputDir + "/recordEncoder.json")) - QFile::remove(outputDir + - "/recordEncoder.json"); - } - - QFile::copy(inputPath + currentProfile + "/basic.ini", - outputDir + "/basic.ini"); - QFile::copy(inputPath + currentProfile + "/service.json", - outputDir + "/service.json"); - QFile::copy(inputPath + currentProfile + "/streamEncoder.json", - outputDir + "/streamEncoder.json"); - QFile::copy(inputPath + currentProfile + "/recordEncoder.json", - outputDir + "/recordEncoder.json"); - } -} - -void OBSBasic::ChangeProfile() -{ - QAction *action = reinterpret_cast(sender()); - ConfigFile config; - std::string path; - - if (!action) - return; - - path = QT_TO_UTF8(action->property("file_name").value()); - if (path.empty()) - return; - - const char *oldName = - config_get_string(App()->GlobalConfig(), "Basic", "Profile"); - if (action->text().compare(QT_UTF8(oldName)) == 0) { - action->setChecked(true); - return; - } - - size_t path_len = path.size(); - path += "/basic.ini"; - - if (config.Open(path.c_str(), CONFIG_OPEN_ALWAYS) != 0) { - blog(LOG_ERROR, "ChangeProfile: Failed to load file '%s'", - path.c_str()); - return; - } - - OnEvent(OBS_FRONTEND_EVENT_PROFILE_CHANGING); - - path.resize(path_len); - - const char *newName = config_get_string(config, "General", "Name"); - const char *newDir = strrchr(path.c_str(), '/') + 1; - - QString settingsRequiringRestart; - bool needsRestart = - ProfileNeedsRestart(config, settingsRequiringRestart); - - config_set_string(App()->GlobalConfig(), "Basic", "Profile", newName); - config_set_string(App()->GlobalConfig(), "Basic", "ProfileDir", newDir); - - Auth::Save(); - auth.reset(); - DestroyPanelCookieManager(); -#ifdef YOUTUBE_ENABLED - if (youtubeAppDock) - DeleteYouTubeAppDock(); -#endif - - config.Swap(basicConfig); - InitBasicConfigDefaults(); - InitBasicConfigDefaults2(); - ResetProfileData(); - RefreshProfiles(); - config_save_safe(App()->GlobalConfig(), "tmp", nullptr); - UpdateTitleBar(); - UpdateVolumeControlsDecayRate(); - - Auth::Load(); -#ifdef YOUTUBE_ENABLED - if (YouTubeAppDock::IsYTServiceSelected() && !youtubeAppDock) - NewYouTubeAppDock(); -#endif - - CheckForSimpleModeX264Fallback(); - - blog(LOG_INFO, "Switched to profile '%s' (%s)", newName, newDir); - blog(LOG_INFO, "------------------------------------------------"); - - OnEvent(OBS_FRONTEND_EVENT_PROFILE_CHANGED); - - if (needsRestart) { - QMessageBox::StandardButton button = OBSMessageBox::question( - this, QTStr("Restart"), - QTStr("LoadProfileNeedsRestart") - .arg(settingsRequiringRestart)); - - if (button == QMessageBox::Yes) { - restart = true; - close(); - } - } + return result; } void OBSBasic::CheckForSimpleModeX264Fallback() { - const char *curStreamEncoder = - config_get_string(basicConfig, "SimpleOutput", "StreamEncoder"); - const char *curRecEncoder = - config_get_string(basicConfig, "SimpleOutput", "RecEncoder"); + const char *curStreamEncoder = config_get_string( + activeConfiguration, "SimpleOutput", "StreamEncoder"); + const char *curRecEncoder = config_get_string( + activeConfiguration, "SimpleOutput", "RecEncoder"); bool qsv_supported = false; bool qsv_av1_supported = false; bool amd_supported = false; @@ -941,11 +976,12 @@ void OBSBasic::CheckForSimpleModeX264Fallback() }; if (!CheckEncoder(curStreamEncoder)) - config_set_string(basicConfig, "SimpleOutput", "StreamEncoder", - curStreamEncoder); + config_set_string(activeConfiguration, "SimpleOutput", + "StreamEncoder", curStreamEncoder); if (!CheckEncoder(curRecEncoder)) - config_set_string(basicConfig, "SimpleOutput", "RecEncoder", - curRecEncoder); - if (changed) - config_save_safe(basicConfig, "tmp", nullptr); + config_set_string(activeConfiguration, "SimpleOutput", + "RecEncoder", curRecEncoder); + if (changed) { + activeConfiguration.SaveSafe("tmp"); + } } diff --git a/UI/window-basic-main.cpp b/UI/window-basic-main.cpp index 19c0481af..c29d7a9d1 100644 --- a/UI/window-basic-main.cpp +++ b/UI/window-basic-main.cpp @@ -162,6 +162,8 @@ template static void SetOBSRef(QListWidgetItem *item, T &&val) QVariant::fromValue(val)); } +constexpr std::string_view OBSProfilePath = "/obs-studio/basic/profiles/"; + static void AddExtraModulePaths() { string plugins_path, plugins_data_path; @@ -786,8 +788,8 @@ void OBSBasic::copyActionsDynamicProperties() void OBSBasic::UpdateVolumeControlsDecayRate() { - double meterDecayRate = - config_get_double(basicConfig, "Audio", "MeterDecayRate"); + double meterDecayRate = config_get_double(activeConfiguration, "Audio", + "MeterDecayRate"); for (size_t i = 0; i < volumes.size(); i++) { volumes[i]->SetMeterDecayRate(meterDecayRate); @@ -797,7 +799,7 @@ void OBSBasic::UpdateVolumeControlsDecayRate() void OBSBasic::UpdateVolumeControlsPeakMeterType() { uint32_t peakMeterTypeIdx = - config_get_uint(basicConfig, "Audio", "PeakMeterType"); + config_get_uint(activeConfiguration, "Audio", "PeakMeterType"); enum obs_peak_meter_type peakMeterType; switch (peakMeterTypeIdx) { @@ -1616,18 +1618,18 @@ retryScene: OnEvent(OBS_FRONTEND_EVENT_PREVIEW_SCENE_CHANGED); } -#define SERVICE_PATH "service.json" +constexpr std::string_view OBSServiceFileName = "service.json"; void OBSBasic::SaveService() { if (!service) return; - char serviceJsonPath[512]; - int ret = GetProfilePath(serviceJsonPath, sizeof(serviceJsonPath), - SERVICE_PATH); - if (ret <= 0) - return; + const OBSProfile ¤tProfile = GetCurrentProfile(); + + const std::filesystem::path jsonFilePath = + currentProfile.path / + std::filesystem::u8path(OBSServiceFileName); OBSDataAutoRelease data = obs_data_create(); OBSDataAutoRelease settings = obs_service_get_settings(service); @@ -1635,26 +1637,35 @@ void OBSBasic::SaveService() obs_data_set_string(data, "type", obs_service_get_type(service)); obs_data_set_obj(data, "settings", settings); - if (!obs_data_save_json_safe(data, serviceJsonPath, "tmp", "bak")) + if (!obs_data_save_json_safe(data, jsonFilePath.u8string().c_str(), + "tmp", "bak")) { blog(LOG_WARNING, "Failed to save service"); + } } bool OBSBasic::LoadService() { + OBSDataAutoRelease data; + + try { + const OBSProfile ¤tProfile = GetCurrentProfile(); + + const std::filesystem::path jsonFilePath = + currentProfile.path / + std::filesystem::u8path(OBSServiceFileName); + + data = obs_data_create_from_json_file_safe( + jsonFilePath.u8string().c_str(), "bak"); + + if (!data) { + return false; + } + } catch (const std::invalid_argument &error) { + blog(LOG_ERROR, "%s", error.what()); + return false; + } + const char *type; - - char serviceJsonPath[512]; - int ret = GetProfilePath(serviceJsonPath, sizeof(serviceJsonPath), - SERVICE_PATH); - if (ret <= 0) - return false; - - OBSDataAutoRelease data = - obs_data_create_from_json_file_safe(serviceJsonPath, "bak"); - - if (!data) - return false; - obs_data_set_default_string(data, "type", "rtmp_common"); type = obs_data_get_string(data, "type"); @@ -1670,19 +1681,20 @@ bool OBSBasic::LoadService() /* Enforce Opus on WHIP if needed */ if (strcmp(obs_service_get_protocol(service), "WHIP") == 0) { - const char *option = config_get_string( - basicConfig, "SimpleOutput", "StreamAudioEncoder"); + const char *option = config_get_string(activeConfiguration, + "SimpleOutput", + "StreamAudioEncoder"); if (strcmp(option, "opus") != 0) - config_set_string(basicConfig, "SimpleOutput", + config_set_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder", "opus"); - option = config_get_string(basicConfig, "AdvOut", + option = config_get_string(activeConfiguration, "AdvOut", "AudioEncoder"); const char *encoder_codec = obs_get_encoder_codec(option); if (!encoder_codec || strcmp(encoder_codec, "opus") != 0) - config_set_string(basicConfig, "AdvOut", "AudioEncoder", - "ffmpeg_opus"); + config_set_string(activeConfiguration, "AdvOut", + "AudioEncoder", "ffmpeg_opus"); } return true; @@ -1751,27 +1763,34 @@ bool OBSBasic::InitBasicConfigDefaults() /* ----------------------------------------------------- */ /* move over old FFmpeg track settings */ - if (config_has_user_value(basicConfig, "AdvOut", "FFAudioTrack") && - !config_has_user_value(basicConfig, "AdvOut", "Pre22.1Settings")) { + if (config_has_user_value(activeConfiguration, "AdvOut", + "FFAudioTrack") && + !config_has_user_value(activeConfiguration, "AdvOut", + "Pre22.1Settings")) { - int track = (int)config_get_int(basicConfig, "AdvOut", + int track = (int)config_get_int(activeConfiguration, "AdvOut", "FFAudioTrack"); - config_set_int(basicConfig, "AdvOut", "FFAudioMixes", + config_set_int(activeConfiguration, "AdvOut", "FFAudioMixes", 1LL << (track - 1)); - config_set_bool(basicConfig, "AdvOut", "Pre22.1Settings", true); + config_set_bool(activeConfiguration, "AdvOut", + "Pre22.1Settings", true); changed = true; } /* ----------------------------------------------------- */ /* move over mixer values in advanced if older config */ - if (config_has_user_value(basicConfig, "AdvOut", "RecTrackIndex") && - !config_has_user_value(basicConfig, "AdvOut", "RecTracks")) { + if (config_has_user_value(activeConfiguration, "AdvOut", + "RecTrackIndex") && + !config_has_user_value(activeConfiguration, "AdvOut", + "RecTracks")) { - uint64_t track = - config_get_uint(basicConfig, "AdvOut", "RecTrackIndex"); + uint64_t track = config_get_uint(activeConfiguration, "AdvOut", + "RecTrackIndex"); track = 1ULL << (track - 1); - config_set_uint(basicConfig, "AdvOut", "RecTracks", track); - config_remove_value(basicConfig, "AdvOut", "RecTrackIndex"); + config_set_uint(activeConfiguration, "AdvOut", "RecTracks", + track); + config_remove_value(activeConfiguration, "AdvOut", + "RecTrackIndex"); changed = true; } @@ -1779,34 +1798,39 @@ bool OBSBasic::InitBasicConfigDefaults() /* set twitch chat extensions to "both" if prev version */ /* is under 24.1 */ if (config_get_bool(App()->GetUserConfig(), "General", - !config_has_user_value(basicConfig, "Twitch", "AddonChoice")) { - config_set_int(basicConfig, "Twitch", "AddonChoice", 3); + "Pre24.1Defaults") && + !config_has_user_value(activeConfiguration, "Twitch", + "AddonChoice")) { + config_set_int(activeConfiguration, "Twitch", "AddonChoice", 3); changed = true; } /* ----------------------------------------------------- */ /* move bitrate enforcement setting to new value */ - if (config_has_user_value(basicConfig, "SimpleOutput", + if (config_has_user_value(activeConfiguration, "SimpleOutput", "EnforceBitrate") && - !config_has_user_value(basicConfig, "Stream1", + !config_has_user_value(activeConfiguration, "Stream1", "IgnoreRecommended") && - !config_has_user_value(basicConfig, "Stream1", "MovedOldEnforce")) { - bool enforce = config_get_bool(basicConfig, "SimpleOutput", - "EnforceBitrate"); - config_set_bool(basicConfig, "Stream1", "IgnoreRecommended", - !enforce); - config_set_bool(basicConfig, "Stream1", "MovedOldEnforce", - true); + !config_has_user_value(activeConfiguration, "Stream1", + "MovedOldEnforce")) { + bool enforce = config_get_bool( + activeConfiguration, "SimpleOutput", "EnforceBitrate"); + config_set_bool(activeConfiguration, "Stream1", + "IgnoreRecommended", !enforce); + config_set_bool(activeConfiguration, "Stream1", + "MovedOldEnforce", true); changed = true; } /* ----------------------------------------------------- */ /* enforce minimum retry delay of 1 second prior to 27.1 */ - if (config_has_user_value(basicConfig, "Output", "RetryDelay")) { - int retryDelay = - config_get_uint(basicConfig, "Output", "RetryDelay"); + if (config_has_user_value(activeConfiguration, "Output", + "RetryDelay")) { + int retryDelay = config_get_uint(activeConfiguration, "Output", + "RetryDelay"); if (retryDelay < 1) { - config_set_uint(basicConfig, "Output", "RetryDelay", 1); + config_set_uint(activeConfiguration, "Output", + "RetryDelay", 1); changed = true; } } @@ -1815,15 +1839,15 @@ bool OBSBasic::InitBasicConfigDefaults() /* Migrate old container selection (if any) to new key. */ auto MigrateFormat = [&](const char *section) { - bool has_old_key = config_has_user_value(basicConfig, section, - "RecFormat"); - bool has_new_key = config_has_user_value(basicConfig, section, - "RecFormat2"); + bool has_old_key = config_has_user_value(activeConfiguration, + section, "RecFormat"); + bool has_new_key = config_has_user_value(activeConfiguration, + section, "RecFormat2"); if (!has_new_key && !has_old_key) return; string old_format = config_get_string( - basicConfig, section, + activeConfiguration, section, has_new_key ? "RecFormat2" : "RecFormat"); string new_format = old_format; if (old_format == "ts") @@ -1836,8 +1860,8 @@ bool OBSBasic::InitBasicConfigDefaults() new_format = "fragmented_mov"; if (new_format != old_format || !has_new_key) { - config_set_string(basicConfig, section, "RecFormat2", - new_format.c_str()); + config_set_string(activeConfiguration, section, + "RecFormat2", new_format.c_str()); changed = true; } }; @@ -1848,137 +1872,175 @@ bool OBSBasic::InitBasicConfigDefaults() /* ----------------------------------------------------- */ /* Migrate output scale setting to GPU scaling options. */ - if (config_get_bool(basicConfig, "AdvOut", "Rescale") && - !config_has_user_value(basicConfig, "AdvOut", "RescaleFilter")) { - config_set_int(basicConfig, "AdvOut", "RescaleFilter", + if (config_get_bool(activeConfiguration, "AdvOut", "Rescale") && + !config_has_user_value(activeConfiguration, "AdvOut", + "RescaleFilter")) { + config_set_int(activeConfiguration, "AdvOut", "RescaleFilter", OBS_SCALE_BILINEAR); } - if (config_get_bool(basicConfig, "AdvOut", "RecRescale") && - !config_has_user_value(basicConfig, "AdvOut", "RecRescaleFilter")) { - config_set_int(basicConfig, "AdvOut", "RecRescaleFilter", - OBS_SCALE_BILINEAR); + if (config_get_bool(activeConfiguration, "AdvOut", "RecRescale") && + !config_has_user_value(activeConfiguration, "AdvOut", + "RecRescaleFilter")) { + config_set_int(activeConfiguration, "AdvOut", + "RecRescaleFilter", OBS_SCALE_BILINEAR); } /* ----------------------------------------------------- */ - if (changed) - config_save_safe(basicConfig, "tmp", nullptr); + if (changed) { + activeConfiguration.SaveSafe("tmp"); + } /* ----------------------------------------------------- */ - config_set_default_string(basicConfig, "Output", "Mode", "Simple"); + config_set_default_string(activeConfiguration, "Output", "Mode", + "Simple"); - config_set_default_bool(basicConfig, "Stream1", "IgnoreRecommended", - false); - config_set_default_bool(basicConfig, "Stream1", "EnableMultitrackVideo", - false); - config_set_default_bool(basicConfig, "Stream1", + config_set_default_bool(activeConfiguration, "Stream1", + "IgnoreRecommended", false); + config_set_default_bool(activeConfiguration, "Stream1", + "EnableMultitrackVideo", false); + config_set_default_bool(activeConfiguration, "Stream1", "MultitrackVideoMaximumAggregateBitrateAuto", true); - config_set_default_bool(basicConfig, "Stream1", + config_set_default_bool(activeConfiguration, "Stream1", "MultitrackVideoMaximumVideoTracksAuto", true); - config_set_default_string(basicConfig, "SimpleOutput", "FilePath", + config_set_default_string(activeConfiguration, "SimpleOutput", + "FilePath", GetDefaultVideoSavePath().c_str()); - config_set_default_string(basicConfig, "SimpleOutput", "RecFormat2", - DEFAULT_CONTAINER); - config_set_default_uint(basicConfig, "SimpleOutput", "VBitrate", 2500); - config_set_default_uint(basicConfig, "SimpleOutput", "ABitrate", 160); - config_set_default_bool(basicConfig, "SimpleOutput", "UseAdvanced", - false); - config_set_default_string(basicConfig, "SimpleOutput", "Preset", + config_set_default_string(activeConfiguration, "SimpleOutput", + "RecFormat2", DEFAULT_CONTAINER); + config_set_default_uint(activeConfiguration, "SimpleOutput", "VBitrate", + 2500); + config_set_default_uint(activeConfiguration, "SimpleOutput", "ABitrate", + 160); + config_set_default_bool(activeConfiguration, "SimpleOutput", + "UseAdvanced", false); + config_set_default_string(activeConfiguration, "SimpleOutput", "Preset", "veryfast"); - config_set_default_string(basicConfig, "SimpleOutput", "NVENCPreset2", - "p5"); - config_set_default_string(basicConfig, "SimpleOutput", "RecQuality", - "Stream"); - config_set_default_bool(basicConfig, "SimpleOutput", "RecRB", false); - config_set_default_int(basicConfig, "SimpleOutput", "RecRBTime", 20); - config_set_default_int(basicConfig, "SimpleOutput", "RecRBSize", 512); - config_set_default_string(basicConfig, "SimpleOutput", "RecRBPrefix", - "Replay"); - config_set_default_string(basicConfig, "SimpleOutput", + config_set_default_string(activeConfiguration, "SimpleOutput", + "NVENCPreset2", "p5"); + config_set_default_string(activeConfiguration, "SimpleOutput", + "RecQuality", "Stream"); + config_set_default_bool(activeConfiguration, "SimpleOutput", "RecRB", + false); + config_set_default_int(activeConfiguration, "SimpleOutput", "RecRBTime", + 20); + config_set_default_int(activeConfiguration, "SimpleOutput", "RecRBSize", + 512); + config_set_default_string(activeConfiguration, "SimpleOutput", + "RecRBPrefix", "Replay"); + config_set_default_string(activeConfiguration, "SimpleOutput", "StreamAudioEncoder", "aac"); - config_set_default_string(basicConfig, "SimpleOutput", + config_set_default_string(activeConfiguration, "SimpleOutput", "RecAudioEncoder", "aac"); - config_set_default_uint(basicConfig, "SimpleOutput", "RecTracks", - (1 << 0)); + config_set_default_uint(activeConfiguration, "SimpleOutput", + "RecTracks", (1 << 0)); - config_set_default_bool(basicConfig, "AdvOut", "ApplyServiceSettings", - true); - config_set_default_bool(basicConfig, "AdvOut", "UseRescale", false); - config_set_default_uint(basicConfig, "AdvOut", "TrackIndex", 1); - config_set_default_uint(basicConfig, "AdvOut", "VodTrackIndex", 2); - config_set_default_string(basicConfig, "AdvOut", "Encoder", "obs_x264"); + config_set_default_bool(activeConfiguration, "AdvOut", + "ApplyServiceSettings", true); + config_set_default_bool(activeConfiguration, "AdvOut", "UseRescale", + false); + config_set_default_uint(activeConfiguration, "AdvOut", "TrackIndex", 1); + config_set_default_uint(activeConfiguration, "AdvOut", "VodTrackIndex", + 2); + config_set_default_string(activeConfiguration, "AdvOut", "Encoder", + "obs_x264"); - config_set_default_string(basicConfig, "AdvOut", "RecType", "Standard"); + config_set_default_string(activeConfiguration, "AdvOut", "RecType", + "Standard"); - config_set_default_string(basicConfig, "AdvOut", "RecFilePath", + config_set_default_string(activeConfiguration, "AdvOut", "RecFilePath", GetDefaultVideoSavePath().c_str()); - config_set_default_string(basicConfig, "AdvOut", "RecFormat2", + config_set_default_string(activeConfiguration, "AdvOut", "RecFormat2", DEFAULT_CONTAINER); - config_set_default_bool(basicConfig, "AdvOut", "RecUseRescale", false); - config_set_default_uint(basicConfig, "AdvOut", "RecTracks", (1 << 0)); - config_set_default_string(basicConfig, "AdvOut", "RecEncoder", "none"); - config_set_default_uint(basicConfig, "AdvOut", "FLVTrack", 1); - config_set_default_uint(basicConfig, "AdvOut", + config_set_default_bool(activeConfiguration, "AdvOut", "RecUseRescale", + false); + config_set_default_uint(activeConfiguration, "AdvOut", "RecTracks", + (1 << 0)); + config_set_default_string(activeConfiguration, "AdvOut", "RecEncoder", + "none"); + config_set_default_uint(activeConfiguration, "AdvOut", "FLVTrack", 1); + config_set_default_uint(activeConfiguration, "AdvOut", "StreamMultiTrackAudioMixes", 1); - config_set_default_bool(basicConfig, "AdvOut", "FFOutputToFile", true); - config_set_default_string(basicConfig, "AdvOut", "FFFilePath", + config_set_default_bool(activeConfiguration, "AdvOut", "FFOutputToFile", + true); + config_set_default_string(activeConfiguration, "AdvOut", "FFFilePath", GetDefaultVideoSavePath().c_str()); - config_set_default_string(basicConfig, "AdvOut", "FFExtension", "mp4"); - config_set_default_uint(basicConfig, "AdvOut", "FFVBitrate", 2500); - config_set_default_uint(basicConfig, "AdvOut", "FFVGOPSize", 250); - config_set_default_bool(basicConfig, "AdvOut", "FFUseRescale", false); - config_set_default_bool(basicConfig, "AdvOut", "FFIgnoreCompat", false); - config_set_default_uint(basicConfig, "AdvOut", "FFABitrate", 160); - config_set_default_uint(basicConfig, "AdvOut", "FFAudioMixes", 1); + config_set_default_string(activeConfiguration, "AdvOut", "FFExtension", + "mp4"); + config_set_default_uint(activeConfiguration, "AdvOut", "FFVBitrate", + 2500); + config_set_default_uint(activeConfiguration, "AdvOut", "FFVGOPSize", + 250); + config_set_default_bool(activeConfiguration, "AdvOut", "FFUseRescale", + false); + config_set_default_bool(activeConfiguration, "AdvOut", "FFIgnoreCompat", + false); + config_set_default_uint(activeConfiguration, "AdvOut", "FFABitrate", + 160); + config_set_default_uint(activeConfiguration, "AdvOut", "FFAudioMixes", + 1); - config_set_default_uint(basicConfig, "AdvOut", "Track1Bitrate", 160); - config_set_default_uint(basicConfig, "AdvOut", "Track2Bitrate", 160); - config_set_default_uint(basicConfig, "AdvOut", "Track3Bitrate", 160); - config_set_default_uint(basicConfig, "AdvOut", "Track4Bitrate", 160); - config_set_default_uint(basicConfig, "AdvOut", "Track5Bitrate", 160); - config_set_default_uint(basicConfig, "AdvOut", "Track6Bitrate", 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track1Bitrate", + 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track2Bitrate", + 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track3Bitrate", + 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track4Bitrate", + 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track5Bitrate", + 160); + config_set_default_uint(activeConfiguration, "AdvOut", "Track6Bitrate", + 160); - config_set_default_uint(basicConfig, "AdvOut", "RecSplitFileTime", 15); - config_set_default_uint(basicConfig, "AdvOut", "RecSplitFileSize", - 2048); + config_set_default_uint(activeConfiguration, "AdvOut", + "RecSplitFileTime", 15); + config_set_default_uint(activeConfiguration, "AdvOut", + "RecSplitFileSize", 2048); - config_set_default_bool(basicConfig, "AdvOut", "RecRB", false); - config_set_default_uint(basicConfig, "AdvOut", "RecRBTime", 20); - config_set_default_int(basicConfig, "AdvOut", "RecRBSize", 512); + config_set_default_bool(activeConfiguration, "AdvOut", "RecRB", false); + config_set_default_uint(activeConfiguration, "AdvOut", "RecRBTime", 20); + config_set_default_int(activeConfiguration, "AdvOut", "RecRBSize", 512); - config_set_default_uint(basicConfig, "Video", "BaseCX", cx); - config_set_default_uint(basicConfig, "Video", "BaseCY", cy); + config_set_default_uint(activeConfiguration, "Video", "BaseCX", cx); + config_set_default_uint(activeConfiguration, "Video", "BaseCY", cy); /* don't allow BaseCX/BaseCY to be susceptible to defaults changing */ - if (!config_has_user_value(basicConfig, "Video", "BaseCX") || - !config_has_user_value(basicConfig, "Video", "BaseCY")) { - config_set_uint(basicConfig, "Video", "BaseCX", cx); - config_set_uint(basicConfig, "Video", "BaseCY", cy); - config_save_safe(basicConfig, "tmp", nullptr); + if (!config_has_user_value(activeConfiguration, "Video", "BaseCX") || + !config_has_user_value(activeConfiguration, "Video", "BaseCY")) { + config_set_uint(activeConfiguration, "Video", "BaseCX", cx); + config_set_uint(activeConfiguration, "Video", "BaseCY", cy); + config_save_safe(activeConfiguration, "tmp", nullptr); } - config_set_default_string(basicConfig, "Output", "FilenameFormatting", + config_set_default_string(activeConfiguration, "Output", + "FilenameFormatting", "%CCYY-%MM-%DD %hh-%mm-%ss"); - config_set_default_bool(basicConfig, "Output", "DelayEnable", false); - config_set_default_uint(basicConfig, "Output", "DelaySec", 20); - config_set_default_bool(basicConfig, "Output", "DelayPreserve", true); + config_set_default_bool(activeConfiguration, "Output", "DelayEnable", + false); + config_set_default_uint(activeConfiguration, "Output", "DelaySec", 20); + config_set_default_bool(activeConfiguration, "Output", "DelayPreserve", + true); - config_set_default_bool(basicConfig, "Output", "Reconnect", true); - config_set_default_uint(basicConfig, "Output", "RetryDelay", 2); - config_set_default_uint(basicConfig, "Output", "MaxRetries", 25); + config_set_default_bool(activeConfiguration, "Output", "Reconnect", + true); + config_set_default_uint(activeConfiguration, "Output", "RetryDelay", 2); + config_set_default_uint(activeConfiguration, "Output", "MaxRetries", + 25); - config_set_default_string(basicConfig, "Output", "BindIP", "default"); - config_set_default_string(basicConfig, "Output", "IPFamily", + config_set_default_string(activeConfiguration, "Output", "BindIP", + "default"); + config_set_default_string(activeConfiguration, "Output", "IPFamily", "IPv4+IPv6"); - config_set_default_bool(basicConfig, "Output", "NewSocketLoopEnable", - false); - config_set_default_bool(basicConfig, "Output", "LowLatencyEnable", - false); + config_set_default_bool(activeConfiguration, "Output", + "NewSocketLoopEnable", false); + config_set_default_bool(activeConfiguration, "Output", + "LowLatencyEnable", false); int i = 0; uint32_t scale_cx = cx; @@ -1992,44 +2054,55 @@ bool OBSBasic::InitBasicConfigDefaults() scale_cy = uint32_t(double(cy) / scale); } - config_set_default_uint(basicConfig, "Video", "OutputCX", scale_cx); - config_set_default_uint(basicConfig, "Video", "OutputCY", scale_cy); + config_set_default_uint(activeConfiguration, "Video", "OutputCX", + scale_cx); + config_set_default_uint(activeConfiguration, "Video", "OutputCY", + scale_cy); /* don't allow OutputCX/OutputCY to be susceptible to defaults * changing */ - if (!config_has_user_value(basicConfig, "Video", "OutputCX") || - !config_has_user_value(basicConfig, "Video", "OutputCY")) { - config_set_uint(basicConfig, "Video", "OutputCX", scale_cx); - config_set_uint(basicConfig, "Video", "OutputCY", scale_cy); - config_save_safe(basicConfig, "tmp", nullptr); + if (!config_has_user_value(activeConfiguration, "Video", "OutputCX") || + !config_has_user_value(activeConfiguration, "Video", "OutputCY")) { + config_set_uint(activeConfiguration, "Video", "OutputCX", + scale_cx); + config_set_uint(activeConfiguration, "Video", "OutputCY", + scale_cy); + config_save_safe(activeConfiguration, "tmp", nullptr); } - config_set_default_uint(basicConfig, "Video", "FPSType", 0); - config_set_default_string(basicConfig, "Video", "FPSCommon", "30"); - config_set_default_uint(basicConfig, "Video", "FPSInt", 30); - config_set_default_uint(basicConfig, "Video", "FPSNum", 30); - config_set_default_uint(basicConfig, "Video", "FPSDen", 1); - config_set_default_string(basicConfig, "Video", "ScaleType", "bicubic"); - config_set_default_string(basicConfig, "Video", "ColorFormat", "NV12"); - config_set_default_string(basicConfig, "Video", "ColorSpace", "709"); - config_set_default_string(basicConfig, "Video", "ColorRange", + config_set_default_uint(activeConfiguration, "Video", "FPSType", 0); + config_set_default_string(activeConfiguration, "Video", "FPSCommon", + "30"); + config_set_default_uint(activeConfiguration, "Video", "FPSInt", 30); + config_set_default_uint(activeConfiguration, "Video", "FPSNum", 30); + config_set_default_uint(activeConfiguration, "Video", "FPSDen", 1); + config_set_default_string(activeConfiguration, "Video", "ScaleType", + "bicubic"); + config_set_default_string(activeConfiguration, "Video", "ColorFormat", + "NV12"); + config_set_default_string(activeConfiguration, "Video", "ColorSpace", + "709"); + config_set_default_string(activeConfiguration, "Video", "ColorRange", "Partial"); - config_set_default_uint(basicConfig, "Video", "SdrWhiteLevel", 300); - config_set_default_uint(basicConfig, "Video", "HdrNominalPeakLevel", - 1000); + config_set_default_uint(activeConfiguration, "Video", "SdrWhiteLevel", + 300); + config_set_default_uint(activeConfiguration, "Video", + "HdrNominalPeakLevel", 1000); - config_set_default_string(basicConfig, "Audio", "MonitoringDeviceId", - "default"); + config_set_default_string(activeConfiguration, "Audio", + "MonitoringDeviceId", "default"); config_set_default_string( - basicConfig, "Audio", "MonitoringDeviceName", + activeConfiguration, "Audio", "MonitoringDeviceName", Str("Basic.Settings.Advanced.Audio.MonitoringDevice" ".Default")); - config_set_default_uint(basicConfig, "Audio", "SampleRate", 48000); - config_set_default_string(basicConfig, "Audio", "ChannelSetup", + config_set_default_uint(activeConfiguration, "Audio", "SampleRate", + 48000); + config_set_default_string(activeConfiguration, "Audio", "ChannelSetup", "Stereo"); - config_set_default_double(basicConfig, "Audio", "MeterDecayRate", - VOLUME_METER_DECAY_FAST); - config_set_default_uint(basicConfig, "Audio", "PeakMeterType", 0); + config_set_default_double(activeConfiguration, "Audio", + "MeterDecayRate", VOLUME_METER_DECAY_FAST); + config_set_default_uint(activeConfiguration, "Audio", "PeakMeterType", + 0); CheckExistingCookieId(); @@ -2044,12 +2117,12 @@ void OBSBasic::InitBasicConfigDefaults2() "Pre23Defaults"); bool useNV = EncoderAvailable("ffmpeg_nvenc") && !oldEncDefaults; - config_set_default_string(basicConfig, "SimpleOutput", "StreamEncoder", - useNV ? SIMPLE_ENCODER_NVENC - : SIMPLE_ENCODER_X264); - config_set_default_string(basicConfig, "SimpleOutput", "RecEncoder", - useNV ? SIMPLE_ENCODER_NVENC - : SIMPLE_ENCODER_X264); + config_set_default_string( + activeConfiguration, "SimpleOutput", "StreamEncoder", + useNV ? SIMPLE_ENCODER_NVENC : SIMPLE_ENCODER_X264); + config_set_default_string( + activeConfiguration, "SimpleOutput", "RecEncoder", + useNV ? SIMPLE_ENCODER_NVENC : SIMPLE_ENCODER_X264); const char *aac_default = "ffmpeg_aac"; if (EncoderAvailable("CoreAudio_AAC")) @@ -2057,47 +2130,37 @@ void OBSBasic::InitBasicConfigDefaults2() else if (EncoderAvailable("libfdk_aac")) aac_default = "libfdk_aac"; - config_set_default_string(basicConfig, "AdvOut", "AudioEncoder", - aac_default); - config_set_default_string(basicConfig, "AdvOut", "RecAudioEncoder", + config_set_default_string(activeConfiguration, "AdvOut", "AudioEncoder", aac_default); + config_set_default_string(activeConfiguration, "AdvOut", + "RecAudioEncoder", aac_default); } bool OBSBasic::InitBasicConfig() { ProfileScope("OBSBasic::InitBasicConfig"); - char configPath[512]; + RefreshProfiles(true); - int ret = GetProfilePath(configPath, sizeof(configPath), ""); - if (ret <= 0) { - OBSErrorBox(nullptr, "Failed to get profile path"); - return false; - } + std::string currentProfileName{ + config_get_string(App()->GetUserConfig(), "Basic", "Profile")}; - if (os_mkdir(configPath) == MKDIR_ERROR) { - OBSErrorBox(nullptr, "Failed to create profile path"); - return false; - } + auto foundProfile = GetProfileByName(currentProfileName); - ret = GetProfilePath(configPath, sizeof(configPath), "basic.ini"); - if (ret <= 0) { - OBSErrorBox(nullptr, "Failed to get basic.ini path"); - return false; - } + if (!foundProfile) { + const OBSProfile &newProfile = + CreateProfile(currentProfileName); - int code = basicConfig.Open(configPath, CONFIG_OPEN_ALWAYS); - if (code != CONFIG_SUCCESS) { - OBSErrorBox(NULL, "Failed to open basic.ini: %d", code); - return false; - } - - if (config_get_string(basicConfig, "General", "Name") == nullptr) { - const char *curName = config_get_string(App()->GlobalConfig(), - "Basic", "Profile"); - - config_set_string(basicConfig, "General", "Name", curName); - basicConfig.SaveSafe("tmp"); + ActivateProfile(newProfile); + } else { + // TODO: Remove duplicate code from OBS initialization and just use ActivateProfile here instead + int code = activeConfiguration.Open( + foundProfile.value().profileFile.u8string().c_str(), + CONFIG_OPEN_ALWAYS); + if (code != CONFIG_SUCCESS) { + OBSErrorBox(NULL, "Failed to open basic.ini: %d", code); + return false; + } } return InitBasicConfigDefaults(); @@ -2200,7 +2263,8 @@ void OBSBasic::ResetOutputs() { ProfileScope("OBSBasic::ResetOutputs"); - const char *mode = config_get_string(basicConfig, "Output", "Mode"); + const char *mode = + config_get_string(activeConfiguration, "Output", "Mode"); bool advOut = astrcmpi(mode, "Advanced") == 0; if ((!outputHandler || !outputHandler->Active()) && @@ -2308,9 +2372,9 @@ void OBSBasic::OBSInit() /* load audio monitoring */ if (obs_audio_monitoring_available()) { const char *device_name = config_get_string( - basicConfig, "Audio", "MonitoringDeviceName"); - const char *device_id = config_get_string(basicConfig, "Audio", - "MonitoringDeviceId"); + activeConfiguration, "Audio", "MonitoringDeviceName"); + const char *device_id = config_get_string( + activeConfiguration, "Audio", "MonitoringDeviceId"); obs_set_audio_monitoring_device(device_name, device_id); @@ -2437,7 +2501,6 @@ void OBSBasic::OBSInit() Q_ARG(bool, true)); RefreshSceneCollections(); - RefreshProfiles(); disableSaving--; auto addDisplay = [this](OBSQTDisplay *window) { @@ -2606,7 +2669,8 @@ void OBSBasic::OBSInit() ToggleMixerLayout(config_get_bool(App()->GetUserConfig(), "BasicWindow", "VerticalVolControl")); - if (config_get_bool(basicConfig, "General", "OpenStatsOnStartup")) + if (config_get_bool(activeConfiguration, "General", + "OpenStatsOnStartup")) on_stats_triggered(); OBSBasicStats::InitializeValues(); @@ -2945,7 +3009,7 @@ void OBSBasic::CreateHotkeys() auto LoadHotkeyData = [&](const char *name) -> OBSData { const char *info = - config_get_string(basicConfig, "Hotkeys", name); + config_get_string(activeConfiguration, "Hotkeys", name); if (!info) return {}; @@ -2967,16 +3031,16 @@ void OBSBasic::CreateHotkeys() const char *name1, const char *oldName = NULL) { if (oldName) { - const auto info = config_get_string(basicConfig, + const auto info = config_get_string(activeConfiguration, "Hotkeys", oldName); if (info) { - config_set_string(basicConfig, "Hotkeys", name0, - info); - config_set_string(basicConfig, "Hotkeys", name1, - info); - config_remove_value(basicConfig, "Hotkeys", - oldName); - config_save(basicConfig); + config_set_string(activeConfiguration, + "Hotkeys", name0, info); + config_set_string(activeConfiguration, + "Hotkeys", name1, info); + config_remove_value(activeConfiguration, + "Hotkeys", oldName); + activeConfiguration.Save(); } } OBSDataArrayAutoRelease array0 = @@ -4205,12 +4269,12 @@ void OBSBasic::ActivateAudioSource(OBSSource source) vol->EnableSlider(!SourceVolumeLocked(source)); - double meterDecayRate = - config_get_double(basicConfig, "Audio", "MeterDecayRate"); + double meterDecayRate = config_get_double(activeConfiguration, "Audio", + "MeterDecayRate"); vol->SetMeterDecayRate(meterDecayRate); uint32_t peakMeterTypeIdx = - config_get_uint(basicConfig, "Audio", "PeakMeterType"); + config_get_uint(activeConfiguration, "Audio", "PeakMeterType"); enum obs_peak_meter_type peakMeterType; switch (peakMeterTypeIdx) { @@ -4922,10 +4986,10 @@ static inline int AttemptToResetVideo(struct obs_video_info *ovi) return obs_reset_video(ovi); } -static inline enum obs_scale_type GetScaleType(ConfigFile &basicConfig) +static inline enum obs_scale_type GetScaleType(ConfigFile &activeConfiguration) { const char *scaleTypeStr = - config_get_string(basicConfig, "Video", "ScaleType"); + config_get_string(activeConfiguration, "Video", "ScaleType"); if (astrcmpi(scaleTypeStr, "bilinear") == 0) return OBS_SCALE_BILINEAR; @@ -5006,43 +5070,43 @@ int OBSBasic::ResetVideo() GetConfigFPS(ovi.fps_num, ovi.fps_den); const char *colorFormat = - config_get_string(basicConfig, "Video", "ColorFormat"); + config_get_string(activeConfiguration, "Video", "ColorFormat"); const char *colorSpace = - config_get_string(basicConfig, "Video", "ColorSpace"); + config_get_string(activeConfiguration, "Video", "ColorSpace"); const char *colorRange = - config_get_string(basicConfig, "Video", "ColorRange"); + config_get_string(activeConfiguration, "Video", "ColorRange"); ovi.graphics_module = App()->GetRenderModule(); - ovi.base_width = - (uint32_t)config_get_uint(basicConfig, "Video", "BaseCX"); - ovi.base_height = - (uint32_t)config_get_uint(basicConfig, "Video", "BaseCY"); - ovi.output_width = - (uint32_t)config_get_uint(basicConfig, "Video", "OutputCX"); - ovi.output_height = - (uint32_t)config_get_uint(basicConfig, "Video", "OutputCY"); + ovi.base_width = (uint32_t)config_get_uint(activeConfiguration, "Video", + "BaseCX"); + ovi.base_height = (uint32_t)config_get_uint(activeConfiguration, + "Video", "BaseCY"); + ovi.output_width = (uint32_t)config_get_uint(activeConfiguration, + "Video", "OutputCX"); + ovi.output_height = (uint32_t)config_get_uint(activeConfiguration, + "Video", "OutputCY"); ovi.output_format = GetVideoFormatFromName(colorFormat); ovi.colorspace = GetVideoColorSpaceFromName(colorSpace); ovi.range = astrcmpi(colorRange, "Full") == 0 ? VIDEO_RANGE_FULL : VIDEO_RANGE_PARTIAL; ovi.adapter = - config_get_uint(App()->GlobalConfig(), "Video", "AdapterIdx"); + config_get_uint(App()->GetUserConfig(), "Video", "AdapterIdx"); ovi.gpu_conversion = true; - ovi.scale_type = GetScaleType(basicConfig); + ovi.scale_type = GetScaleType(activeConfiguration); if (ovi.base_width < 32 || ovi.base_height < 32) { ovi.base_width = 1920; ovi.base_height = 1080; - config_set_uint(basicConfig, "Video", "BaseCX", 1920); - config_set_uint(basicConfig, "Video", "BaseCY", 1080); + config_set_uint(activeConfiguration, "Video", "BaseCX", 1920); + config_set_uint(activeConfiguration, "Video", "BaseCY", 1080); } if (ovi.output_width < 32 || ovi.output_height < 32) { ovi.output_width = ovi.base_width; ovi.output_height = ovi.base_height; - config_set_uint(basicConfig, "Video", "OutputCX", + config_set_uint(activeConfiguration, "Video", "OutputCX", ovi.base_width); - config_set_uint(basicConfig, "Video", "OutputCY", + config_set_uint(activeConfiguration, "Video", "OutputCY", ovi.base_height); } @@ -5058,9 +5122,9 @@ int OBSBasic::ResetVideo() ResizeProgram(ovi.base_width, ovi.base_height); const float sdr_white_level = (float)config_get_uint( - basicConfig, "Video", "SdrWhiteLevel"); + activeConfiguration, "Video", "SdrWhiteLevel"); const float hdr_nominal_peak_level = (float)config_get_uint( - basicConfig, "Video", "HdrNominalPeakLevel"); + activeConfiguration, "Video", "HdrNominalPeakLevel"); obs_set_video_levels(sdr_white_level, hdr_nominal_peak_level); OBSBasicStats::InitializeValues(); OBSProjector::UpdateMultiviewProjectors(); @@ -5085,10 +5149,10 @@ bool OBSBasic::ResetAudio() struct obs_audio_info2 ai = {}; ai.samples_per_sec = - config_get_uint(basicConfig, "Audio", "SampleRate"); + config_get_uint(activeConfiguration, "Audio", "SampleRate"); const char *channelSetupStr = - config_get_string(basicConfig, "Audio", "ChannelSetup"); + config_get_string(activeConfiguration, "Audio", "ChannelSetup"); if (strcmp(channelSetupStr, "Mono") == 0) ai.speakers = SPEAKERS_MONO; @@ -5519,15 +5583,18 @@ void OBSBasic::changeEvent(QEvent *event) void OBSBasic::on_actionShow_Recordings_triggered() { - const char *mode = config_get_string(basicConfig, "Output", "Mode"); - const char *type = config_get_string(basicConfig, "AdvOut", "RecType"); + const char *mode = + config_get_string(activeConfiguration, "Output", "Mode"); + const char *type = + config_get_string(activeConfiguration, "AdvOut", "RecType"); const char *adv_path = strcmp(type, "Standard") - ? config_get_string(basicConfig, "AdvOut", "FFFilePath") - : config_get_string(basicConfig, "AdvOut", + ? config_get_string(activeConfiguration, "AdvOut", + "FFFilePath") + : config_get_string(activeConfiguration, "AdvOut", "RecFilePath"); const char *path = strcmp(mode, "Advanced") - ? config_get_string(basicConfig, + ? config_get_string(activeConfiguration, "SimpleOutput", "FilePath") : adv_path; @@ -5542,13 +5609,14 @@ void OBSBasic::on_actionRemux_triggered() return; } - const char *mode = config_get_string(basicConfig, "Output", "Mode"); + const char *mode = + config_get_string(activeConfiguration, "Output", "Mode"); const char *path = strcmp(mode, "Advanced") - ? config_get_string(basicConfig, + ? config_get_string(activeConfiguration, "SimpleOutput", "FilePath") - : config_get_string(basicConfig, "AdvOut", - "RecFilePath"); + : config_get_string(activeConfiguration, + "AdvOut", "RecFilePath"); OBSRemux *remuxDlg; remuxDlg = new OBSRemux(path, this); @@ -8507,12 +8575,16 @@ void OBSBasic::on_actionShowSettingsFolder_triggered() void OBSBasic::on_actionShowProfileFolder_triggered() { - char path[512]; - int ret = GetProfilePath(path, 512, ""); - if (ret <= 0) - return; + std::string userProfilePath; + userProfilePath.reserve(App()->userProfilesLocation.u8string().size() + + OBSProfilePath.size()); + userProfilePath.append(App()->userProfilesLocation.u8string()) + .append(OBSProfilePath); - QDesktopServices::openUrl(QUrl::fromLocalFile(path)); + const QString userProfileLocation = + QString::fromStdString(userProfilePath); + + QDesktopServices::openUrl(QUrl::fromLocalFile(userProfileLocation)); } int OBSBasic::GetTopSelectedSourceItem() @@ -8599,7 +8671,8 @@ void OBSBasic::ToggleAlwaysOnTop() void OBSBasic::GetFPSCommon(uint32_t &num, uint32_t &den) const { - const char *val = config_get_string(basicConfig, "Video", "FPSCommon"); + const char *val = + config_get_string(activeConfiguration, "Video", "FPSCommon"); if (strcmp(val, "10") == 0) { num = 10; @@ -8636,25 +8709,26 @@ void OBSBasic::GetFPSCommon(uint32_t &num, uint32_t &den) const void OBSBasic::GetFPSInteger(uint32_t &num, uint32_t &den) const { - num = (uint32_t)config_get_uint(basicConfig, "Video", "FPSInt"); + num = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSInt"); den = 1; } void OBSBasic::GetFPSFraction(uint32_t &num, uint32_t &den) const { - num = (uint32_t)config_get_uint(basicConfig, "Video", "FPSNum"); - den = (uint32_t)config_get_uint(basicConfig, "Video", "FPSDen"); + num = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSNum"); + den = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSDen"); } void OBSBasic::GetFPSNanoseconds(uint32_t &num, uint32_t &den) const { num = 1000000000; - den = (uint32_t)config_get_uint(basicConfig, "Video", "FPSNS"); + den = (uint32_t)config_get_uint(activeConfiguration, "Video", "FPSNS"); } void OBSBasic::GetConfigFPS(uint32_t &num, uint32_t &den) const { - uint32_t type = config_get_uint(basicConfig, "Video", "FPSType"); + uint32_t type = + config_get_uint(activeConfiguration, "Video", "FPSType"); if (type == 1) //"Integer" GetFPSInteger(num, den); @@ -8670,7 +8744,7 @@ void OBSBasic::GetConfigFPS(uint32_t &num, uint32_t &den) const config_t *OBSBasic::Config() const { - return basicConfig; + return activeConfiguration; } #ifdef YOUTUBE_ENABLED @@ -10589,14 +10663,14 @@ void OBSBasic::ResizeOutputSizeOfSource() int width = obs_source_get_width(source); int height = obs_source_get_height(source); - config_set_uint(basicConfig, "Video", "BaseCX", width); - config_set_uint(basicConfig, "Video", "BaseCY", height); - config_set_uint(basicConfig, "Video", "OutputCX", width); - config_set_uint(basicConfig, "Video", "OutputCY", height); + config_set_uint(activeConfiguration, "Video", "BaseCX", width); + config_set_uint(activeConfiguration, "Video", "BaseCY", height); + config_set_uint(activeConfiguration, "Video", "OutputCX", width); + config_set_uint(activeConfiguration, "Video", "OutputCY", height); ResetVideo(); ResetOutputs(); - config_save_safe(basicConfig, "tmp", nullptr); + activeConfiguration.SaveSafe("tmp"); on_actionFitToScreen_triggered(); } @@ -10929,25 +11003,26 @@ void OBSBasic::RecordPauseToggled() void OBSBasic::UpdateIsRecordingPausable() { - const char *mode = config_get_string(basicConfig, "Output", "Mode"); + const char *mode = + config_get_string(activeConfiguration, "Output", "Mode"); bool adv = astrcmpi(mode, "Advanced") == 0; bool shared = true; if (adv) { - const char *recType = - config_get_string(basicConfig, "AdvOut", "RecType"); + const char *recType = config_get_string(activeConfiguration, + "AdvOut", "RecType"); if (astrcmpi(recType, "FFmpeg") == 0) { - shared = config_get_bool(basicConfig, "AdvOut", + shared = config_get_bool(activeConfiguration, "AdvOut", "FFOutputToFile"); } else { const char *recordEncoder = config_get_string( - basicConfig, "AdvOut", "RecEncoder"); + activeConfiguration, "AdvOut", "RecEncoder"); shared = astrcmpi(recordEncoder, "none") == 0; } } else { const char *quality = config_get_string( - basicConfig, "SimpleOutput", "RecQuality"); + activeConfiguration, "SimpleOutput", "RecQuality"); shared = strcmp(quality, "Stream") == 0; } @@ -11272,3 +11347,57 @@ void OBSBasic::PreviewScalingModeChanged(int value) break; }; } + +// MARK: - Generic UI Helper Functions + +OBSPromptResult OBSBasic::PromptForName(const OBSPromptRequest &request, + const OBSPromptCallback &callback) +{ + OBSPromptResult result; + + for (;;) { + result.success = false; + + if (request.withOption && !request.optionPrompt.empty()) { + result.optionValue = request.optionValue; + + result.success = NameDialog::AskForNameWithOption( + this, request.title.c_str(), + request.prompt.c_str(), result.promptValue, + request.optionPrompt.c_str(), + result.optionValue, + (request.promptValue.empty() + ? nullptr + : request.promptValue.c_str())); + + } else { + result.success = NameDialog::AskForName( + this, request.title.c_str(), + request.prompt.c_str(), result.promptValue, + (request.promptValue.empty() + ? nullptr + : request.promptValue.c_str())); + } + + if (!result.success) { + break; + } + + if (result.promptValue.empty()) { + OBSMessageBox::warning(this, + QTStr("NoNameEntered.Title"), + QTStr("NoNameEntered.Text")); + continue; + } + + if (!callback(result)) { + OBSMessageBox::warning(this, QTStr("NameExists.Title"), + QTStr("NameExists.Text")); + continue; + } + + break; + } + + return result; +} diff --git a/UI/window-basic-main.hpp b/UI/window-basic-main.hpp index c1d21c991..4bda02c20 100644 --- a/UI/window-basic-main.hpp +++ b/UI/window-basic-main.hpp @@ -134,6 +134,37 @@ private: std::shared_ptr renamedSignal; }; +struct OBSProfile { + std::string name; + std::string directoryName; + std::filesystem::path path; + std::filesystem::path profileFile; +}; + +struct OBSSceneCollection { + std::string name; + std::string fileName; + std::filesystem::path collectionFile; +}; + +struct OBSPromptResult { + bool success; + std::string promptValue; + bool optionValue; +}; + +struct OBSPromptRequest { + std::string title; + std::string prompt; + std::string promptValue; + bool withOption; + std::string optionPrompt; + bool optionValue; +}; + +using OBSPromptCallback = std::function; + +using OBSProfileCache = std::map; class ColorSelect : public QWidget { public: @@ -443,17 +474,6 @@ private: void RefreshSceneCollections(); void ChangeSceneCollection(); void LogScenes(); - - void ResetProfileData(); - bool AddProfile(bool create_new, const char *title, const char *text, - const char *init_text = nullptr, bool rename = false); - bool CreateProfile(const std::string &newName, bool create_new, - bool showWizardChecked, bool rename = false); - void DeleteProfile(const char *profile_name, const char *profile_dir); - void RefreshProfiles(); - void ChangeProfile(); - void CheckForSimpleModeX264Fallback(); - void SaveProjectNow(); int GetTopSelectedSourceItem(); @@ -742,11 +762,6 @@ public slots: bool AddSceneCollection(bool create_new, const QString &name = QString()); - - bool NewProfile(const QString &name); - bool DuplicateProfile(const QString &name); - void DeleteProfile(const QString &profileName); - void UpdatePatronJson(const QString &text, const QString &error); void ShowContextBar(); @@ -1156,13 +1171,6 @@ private slots: void on_actionExportSceneCollection_triggered(); void on_actionRemigrateSceneCollection_triggered(); - void on_actionNewProfile_triggered(); - void on_actionDupProfile_triggered(); - void on_actionRenameProfile_triggered(); - void on_actionRemoveProfile_triggered(bool skipConfirmation = false); - void on_actionImportProfile_triggered(); - void on_actionExportProfile_triggered(); - void on_actionShowSettingsFolder_triggered(); void on_actionShowProfileFolder_triggered(); @@ -1337,6 +1345,59 @@ public: void DeleteYouTubeAppDock(); YouTubeAppDock *GetYouTubeAppDock(); #endif + // MARK: - Generic UI Helper Functions + OBSPromptResult PromptForName(const OBSPromptRequest &request, + const OBSPromptCallback &callback); + + // MARK: - OBS Profile Management +private: + OBSProfileCache profiles{}; + + void SetupNewProfile(const std::string &profileName, + bool useWizard = false); + void SetupDuplicateProfile(const std::string &profileName); + void SetupRenameProfile(const std::string &profileName); + + const OBSProfile &CreateProfile(const std::string &profileName); + void RemoveProfile(OBSProfile profile); + + void ChangeProfile(); + + void RefreshProfileCache(); + + void RefreshProfiles(bool refreshCache = false); + + void ActivateProfile(const OBSProfile &profile, bool reset = false); + std::vector + GetRestartRequirements(const ConfigFile &config) const; + void ResetProfileData(); + void CheckForSimpleModeX264Fallback(); + +public: + inline const OBSProfileCache &GetProfileCache() const noexcept + { + return profiles; + }; + + const OBSProfile &GetCurrentProfile() const; + + std::optional + GetProfileByName(const std::string &profileName) const; + std::optional + GetProfileByDirectoryName(const std::string &directoryName) const; + +private slots: + void on_actionNewProfile_triggered(); + void on_actionDupProfile_triggered(); + void on_actionRenameProfile_triggered(); + void on_actionRemoveProfile_triggered(bool skipConfirmation = false); + void on_actionImportProfile_triggered(); + void on_actionExportProfile_triggered(); + +public slots: + bool CreateNewProfile(const QString &name); + bool CreateDuplicateProfile(const QString &name); + void DeleteProfile(const QString &profileName); }; extern bool cef_js_avail; diff --git a/UI/window-basic-settings.cpp b/UI/window-basic-settings.cpp index 7cb863044..924405077 100644 --- a/UI/window-basic-settings.cpp +++ b/UI/window-basic-settings.cpp @@ -2136,12 +2136,16 @@ OBSBasicSettings::CreateEncoderPropertyView(const char *encoder, OBSPropertiesView *view; if (path) { - char encoderJsonPath[512]; - int ret = GetProfilePath(encoderJsonPath, - sizeof(encoderJsonPath), path); - if (ret > 0) { + const OBSBasic *basic = + reinterpret_cast(App()->GetMainWindow()); + const OBSProfile ¤tProfile = basic->GetCurrentProfile(); + + const std::filesystem::path jsonFilePath = + currentProfile.path / std::filesystem::u8path(path); + + if (!jsonFilePath.empty()) { obs_data_t *data = obs_data_create_from_json_file_safe( - encoderJsonPath, "bak"); + jsonFilePath.u8string().c_str(), "bak"); obs_data_apply(settings, data); obs_data_release(data); } @@ -3748,17 +3752,22 @@ static inline const char *SplitFileTypeFromIdx(int idx) static void WriteJsonData(OBSPropertiesView *view, const char *path) { - char full_path[512]; - if (!view || !WidgetChanged(view)) return; - int ret = GetProfilePath(full_path, sizeof(full_path), path); - if (ret > 0) { + const OBSBasic *basic = + reinterpret_cast(App()->GetMainWindow()); + const OBSProfile ¤tProfile = basic->GetCurrentProfile(); + + const std::filesystem::path jsonFilePath = + currentProfile.path / std::filesystem::u8path(path); + + if (!jsonFilePath.empty()) { obs_data_t *settings = view->GetSettings(); if (settings) { - obs_data_save_json_safe(settings, full_path, "tmp", - "bak"); + obs_data_save_json_safe(settings, + jsonFilePath.u8string().c_str(), + "tmp", "bak"); } } } @@ -5691,14 +5700,16 @@ void OBSBasicSettings::AdvReplayBufferChanged() if (!settings) return; - char encoderJsonPath[512]; - int ret = GetProfilePath(encoderJsonPath, - sizeof(encoderJsonPath), - "recordEncoder.json"); - if (ret > 0) { + const OBSProfile ¤tProfile = main->GetCurrentProfile(); + + const std::filesystem::path jsonFilePath = + currentProfile.path / + std::filesystem::u8path("recordEncoder.json"); + + if (!jsonFilePath.empty()) { OBSDataAutoRelease data = obs_data_create_from_json_file_safe( - encoderJsonPath, "bak"); + jsonFilePath.u8string().c_str(), "bak"); obs_data_apply(settings, data); } } diff --git a/UI/window-youtube-actions.cpp b/UI/window-youtube-actions.cpp index e2d1926df..574881e6e 100644 --- a/UI/window-youtube-actions.cpp +++ b/UI/window-youtube-actions.cpp @@ -287,8 +287,8 @@ OBSYoutubeActions::OBSYoutubeActions(QWidget *parent, Auth *auth, workerThread->start(); OBSBasic *main = OBSBasic::Get(); - bool rememberSettings = config_get_bool(main->basicConfig, "YouTube", - "RememberSettings"); + bool rememberSettings = config_get_bool(main->activeConfiguration, + "YouTube", "RememberSettings"); if (rememberSettings) LoadSettings(); @@ -749,83 +749,85 @@ void OBSYoutubeActions::SaveSettings(BroadcastDescription &broadcast) { OBSBasic *main = OBSBasic::Get(); - config_set_string(main->basicConfig, "YouTube", "Title", + config_set_string(main->activeConfiguration, "YouTube", "Title", QT_TO_UTF8(broadcast.title)); - config_set_string(main->basicConfig, "YouTube", "Description", + config_set_string(main->activeConfiguration, "YouTube", "Description", QT_TO_UTF8(broadcast.description)); - config_set_string(main->basicConfig, "YouTube", "Privacy", + config_set_string(main->activeConfiguration, "YouTube", "Privacy", QT_TO_UTF8(broadcast.privacy)); - config_set_string(main->basicConfig, "YouTube", "CategoryID", + config_set_string(main->activeConfiguration, "YouTube", "CategoryID", QT_TO_UTF8(broadcast.category.id)); - config_set_string(main->basicConfig, "YouTube", "Latency", + config_set_string(main->activeConfiguration, "YouTube", "Latency", QT_TO_UTF8(broadcast.latency)); - config_set_bool(main->basicConfig, "YouTube", "MadeForKids", + config_set_bool(main->activeConfiguration, "YouTube", "MadeForKids", broadcast.made_for_kids); - config_set_bool(main->basicConfig, "YouTube", "AutoStart", + config_set_bool(main->activeConfiguration, "YouTube", "AutoStart", broadcast.auto_start); - config_set_bool(main->basicConfig, "YouTube", "AutoStop", + config_set_bool(main->activeConfiguration, "YouTube", "AutoStop", broadcast.auto_start); - config_set_bool(main->basicConfig, "YouTube", "DVR", broadcast.dvr); - config_set_bool(main->basicConfig, "YouTube", "ScheduleForLater", - broadcast.schedul_for_later); - config_set_string(main->basicConfig, "YouTube", "Projection", + config_set_bool(main->activeConfiguration, "YouTube", "DVR", + broadcast.dvr); + config_set_bool(main->activeConfiguration, "YouTube", + "ScheduleForLater", broadcast.schedul_for_later); + config_set_string(main->activeConfiguration, "YouTube", "Projection", QT_TO_UTF8(broadcast.projection)); - config_set_string(main->basicConfig, "YouTube", "ThumbnailFile", + config_set_string(main->activeConfiguration, "YouTube", "ThumbnailFile", QT_TO_UTF8(thumbnailFile)); - config_set_bool(main->basicConfig, "YouTube", "RememberSettings", true); + config_set_bool(main->activeConfiguration, "YouTube", + "RememberSettings", true); } void OBSYoutubeActions::LoadSettings() { OBSBasic *main = OBSBasic::Get(); - const char *title = - config_get_string(main->basicConfig, "YouTube", "Title"); + const char *title = config_get_string(main->activeConfiguration, + "YouTube", "Title"); ui->title->setText(QT_UTF8(title)); - const char *desc = - config_get_string(main->basicConfig, "YouTube", "Description"); + const char *desc = config_get_string(main->activeConfiguration, + "YouTube", "Description"); ui->description->setPlainText(QT_UTF8(desc)); - const char *priv = - config_get_string(main->basicConfig, "YouTube", "Privacy"); + const char *priv = config_get_string(main->activeConfiguration, + "YouTube", "Privacy"); int index = ui->privacyBox->findData(priv); ui->privacyBox->setCurrentIndex(index); - const char *catID = - config_get_string(main->basicConfig, "YouTube", "CategoryID"); + const char *catID = config_get_string(main->activeConfiguration, + "YouTube", "CategoryID"); index = ui->categoryBox->findData(catID); ui->categoryBox->setCurrentIndex(index); - const char *latency = - config_get_string(main->basicConfig, "YouTube", "Latency"); + const char *latency = config_get_string(main->activeConfiguration, + "YouTube", "Latency"); index = ui->latencyBox->findData(latency); ui->latencyBox->setCurrentIndex(index); - bool dvr = config_get_bool(main->basicConfig, "YouTube", "DVR"); + bool dvr = config_get_bool(main->activeConfiguration, "YouTube", "DVR"); ui->checkDVR->setChecked(dvr); - bool forKids = - config_get_bool(main->basicConfig, "YouTube", "MadeForKids"); + bool forKids = config_get_bool(main->activeConfiguration, "YouTube", + "MadeForKids"); if (forKids) ui->yesMakeForKids->setChecked(true); else ui->notMakeForKids->setChecked(true); - bool schedLater = config_get_bool(main->basicConfig, "YouTube", + bool schedLater = config_get_bool(main->activeConfiguration, "YouTube", "ScheduleForLater"); ui->checkScheduledLater->setChecked(schedLater); - bool autoStart = - config_get_bool(main->basicConfig, "YouTube", "AutoStart"); + bool autoStart = config_get_bool(main->activeConfiguration, "YouTube", + "AutoStart"); ui->checkAutoStart->setChecked(autoStart); - bool autoStop = - config_get_bool(main->basicConfig, "YouTube", "AutoStop"); + bool autoStop = config_get_bool(main->activeConfiguration, "YouTube", + "AutoStop"); ui->checkAutoStop->setChecked(autoStop); - const char *projection = - config_get_string(main->basicConfig, "YouTube", "Projection"); + const char *projection = config_get_string(main->activeConfiguration, + "YouTube", "Projection"); if (projection && *projection) { if (strcmp(projection, "360") == 0) ui->check360Video->setChecked(true); @@ -833,8 +835,8 @@ void OBSYoutubeActions::LoadSettings() ui->check360Video->setChecked(false); } - const char *thumbFile = config_get_string(main->basicConfig, "YouTube", - "ThumbnailFile"); + const char *thumbFile = config_get_string(main->activeConfiguration, + "YouTube", "ThumbnailFile"); if (thumbFile && *thumbFile) { QFileInfo tFile(thumbFile); // Re-check validity before setting path again