From bcc6880183782e9999eb43e7ed90ec1a5982db0e Mon Sep 17 00:00:00 2001 From: PatTheMav Date: Mon, 9 Dec 2024 19:19:27 +0100 Subject: [PATCH] frontend: Prepare main application implementation for splits --- UI/obs-app.cpp => frontend/OBSApp.cpp | 0 UI/obs-app.hpp => frontend/OBSApp.hpp | 0 .../OBSApp_Themes.cpp | 0 .../OBSStudioAPI.cpp | 0 frontend/OBSStudioAPI.hpp | 636 ++++ frontend/obs-main.cpp | 2662 +++++++++++++++++ frontend/utility/BaseLexer.hpp | 280 ++ .../utility/OBSTheme.hpp | 0 frontend/utility/OBSThemeVariable.hpp | 66 + frontend/utility/OBSTranslator.cpp | 2662 +++++++++++++++++ frontend/utility/OBSTranslator.hpp | 280 ++ 11 files changed, 6586 insertions(+) rename UI/obs-app.cpp => frontend/OBSApp.cpp (100%) rename UI/obs-app.hpp => frontend/OBSApp.hpp (100%) rename UI/obs-app-theming.cpp => frontend/OBSApp_Themes.cpp (100%) rename UI/api-interface.cpp => frontend/OBSStudioAPI.cpp (100%) create mode 100644 frontend/OBSStudioAPI.hpp create mode 100644 frontend/obs-main.cpp create mode 100644 frontend/utility/BaseLexer.hpp rename UI/obs-app-theming.hpp => frontend/utility/OBSTheme.hpp (100%) create mode 100644 frontend/utility/OBSThemeVariable.hpp create mode 100644 frontend/utility/OBSTranslator.cpp create mode 100644 frontend/utility/OBSTranslator.hpp diff --git a/UI/obs-app.cpp b/frontend/OBSApp.cpp similarity index 100% rename from UI/obs-app.cpp rename to frontend/OBSApp.cpp diff --git a/UI/obs-app.hpp b/frontend/OBSApp.hpp similarity index 100% rename from UI/obs-app.hpp rename to frontend/OBSApp.hpp diff --git a/UI/obs-app-theming.cpp b/frontend/OBSApp_Themes.cpp similarity index 100% rename from UI/obs-app-theming.cpp rename to frontend/OBSApp_Themes.cpp diff --git a/UI/api-interface.cpp b/frontend/OBSStudioAPI.cpp similarity index 100% rename from UI/api-interface.cpp rename to frontend/OBSStudioAPI.cpp diff --git a/frontend/OBSStudioAPI.hpp b/frontend/OBSStudioAPI.hpp new file mode 100644 index 000000000..bb3715017 --- /dev/null +++ b/frontend/OBSStudioAPI.hpp @@ -0,0 +1,636 @@ +#include +#include +#include "obs-app.hpp" +#include "window-basic-main.hpp" +#include "window-basic-main-outputs.hpp" + +#include + +using namespace std; + +Q_DECLARE_METATYPE(OBSScene); +Q_DECLARE_METATYPE(OBSSource); + +template static T GetOBSRef(QListWidgetItem *item) +{ + return item->data(static_cast(QtDataRole::OBSRef)).value(); +} + +extern volatile bool streaming_active; +extern volatile bool recording_active; +extern volatile bool recording_paused; +extern volatile bool replaybuf_active; +extern volatile bool virtualcam_active; + +/* ------------------------------------------------------------------------- */ + +template struct OBSStudioCallback { + T callback; + void *private_data; + + inline OBSStudioCallback(T cb, void *p) : callback(cb), private_data(p) {} +}; + +template +inline size_t GetCallbackIdx(vector> &callbacks, T callback, void *private_data) +{ + for (size_t i = 0; i < callbacks.size(); i++) { + OBSStudioCallback curCB = callbacks[i]; + if (curCB.callback == callback && curCB.private_data == private_data) + return i; + } + + return (size_t)-1; +} + +struct OBSStudioAPI : obs_frontend_callbacks { + OBSBasic *main; + vector> callbacks; + vector> saveCallbacks; + vector> preloadCallbacks; + + inline OBSStudioAPI(OBSBasic *main_) : main(main_) {} + + void *obs_frontend_get_main_window(void) override { return (void *)main; } + + void *obs_frontend_get_main_window_handle(void) override { return (void *)main->winId(); } + + void *obs_frontend_get_system_tray(void) override { return (void *)main->trayIcon.data(); } + + void obs_frontend_get_scenes(struct obs_frontend_source_list *sources) override + { + for (int i = 0; i < main->ui->scenes->count(); i++) { + QListWidgetItem *item = main->ui->scenes->item(i); + OBSScene scene = GetOBSRef(item); + obs_source_t *source = obs_scene_get_source(scene); + + if (obs_source_get_ref(source) != nullptr) + da_push_back(sources->sources, &source); + } + } + + obs_source_t *obs_frontend_get_current_scene(void) override + { + if (main->IsPreviewProgramMode()) { + return obs_weak_source_get_source(main->programScene); + } else { + OBSSource source = main->GetCurrentSceneSource(); + return obs_source_get_ref(source); + } + } + + void obs_frontend_set_current_scene(obs_source_t *scene) override + { + if (main->IsPreviewProgramMode()) { + QMetaObject::invokeMethod(main, "TransitionToScene", WaitConnection(), + Q_ARG(OBSSource, OBSSource(scene))); + } else { + QMetaObject::invokeMethod(main, "SetCurrentScene", WaitConnection(), + Q_ARG(OBSSource, OBSSource(scene)), Q_ARG(bool, false)); + } + } + + void obs_frontend_get_transitions(struct obs_frontend_source_list *sources) override + { + for (int i = 0; i < main->ui->transitions->count(); i++) { + OBSSource tr = main->ui->transitions->itemData(i).value(); + + if (!tr) + continue; + + if (obs_source_get_ref(tr) != nullptr) + da_push_back(sources->sources, &tr); + } + } + + obs_source_t *obs_frontend_get_current_transition(void) override + { + OBSSource tr = main->GetCurrentTransition(); + return obs_source_get_ref(tr); + } + + void obs_frontend_set_current_transition(obs_source_t *transition) override + { + QMetaObject::invokeMethod(main, "SetTransition", Q_ARG(OBSSource, OBSSource(transition))); + } + + int obs_frontend_get_transition_duration(void) override { return main->ui->transitionDuration->value(); } + + void obs_frontend_set_transition_duration(int duration) override + { + QMetaObject::invokeMethod(main->ui->transitionDuration, "setValue", Q_ARG(int, duration)); + } + + void obs_frontend_release_tbar(void) override { QMetaObject::invokeMethod(main, "TBarReleased"); } + + void obs_frontend_set_tbar_position(int position) override + { + QMetaObject::invokeMethod(main, "TBarChanged", Q_ARG(int, position)); + } + + int obs_frontend_get_tbar_position(void) override { return main->tBar->value(); } + + void obs_frontend_get_scene_collections(std::vector &strings) override + { + for (auto &[collectionName, collection] : main->GetSceneCollectionCache()) { + strings.emplace_back(collectionName); + } + } + + char *obs_frontend_get_current_scene_collection(void) override + { + const OBSSceneCollection ¤tCollection = main->GetCurrentSceneCollection(); + return bstrdup(currentCollection.name.c_str()); + } + + void obs_frontend_set_current_scene_collection(const char *collection) override + { + QList menuActions = main->ui->sceneCollectionMenu->actions(); + QString qstrCollection = QT_UTF8(collection); + + for (int i = 0; i < menuActions.count(); i++) { + QAction *action = menuActions[i]; + QVariant v = action->property("file_name"); + + if (v.typeName() != nullptr) { + if (action->text() == qstrCollection) { + action->trigger(); + break; + } + } + } + } + + bool obs_frontend_add_scene_collection(const char *name) override + { + bool success = false; + QMetaObject::invokeMethod(main, "CreateNewSceneCollection", WaitConnection(), + Q_RETURN_ARG(bool, success), Q_ARG(QString, QT_UTF8(name))); + return success; + } + + void obs_frontend_get_profiles(std::vector &strings) override + { + const OBSProfileCache &profiles = main->GetProfileCache(); + + for (auto &[profileName, profile] : profiles) { + strings.emplace_back(profileName); + } + } + + char *obs_frontend_get_current_profile(void) override + { + const OBSProfile &profile = main->GetCurrentProfile(); + return bstrdup(profile.name.c_str()); + } + + char *obs_frontend_get_current_profile_path(void) override + { + const OBSProfile &profile = main->GetCurrentProfile(); + + return bstrdup(profile.path.u8string().c_str()); + } + + void obs_frontend_set_current_profile(const char *profile) override + { + QList menuActions = main->ui->profileMenu->actions(); + QString qstrProfile = QT_UTF8(profile); + + for (int i = 0; i < menuActions.count(); i++) { + QAction *action = menuActions[i]; + QVariant v = action->property("file_name"); + + if (v.typeName() != nullptr) { + if (action->text() == qstrProfile) { + action->trigger(); + break; + } + } + } + } + + void obs_frontend_create_profile(const char *name) override + { + QMetaObject::invokeMethod(main, "CreateNewProfile", Q_ARG(QString, name)); + } + + void obs_frontend_duplicate_profile(const char *name) override + { + QMetaObject::invokeMethod(main, "CreateDuplicateProfile", Q_ARG(QString, name)); + } + + void obs_frontend_delete_profile(const char *profile) override + { + QMetaObject::invokeMethod(main, "DeleteProfile", Q_ARG(QString, profile)); + } + + void obs_frontend_streaming_start(void) override { QMetaObject::invokeMethod(main, "StartStreaming"); } + + void obs_frontend_streaming_stop(void) override { QMetaObject::invokeMethod(main, "StopStreaming"); } + + bool obs_frontend_streaming_active(void) override { return os_atomic_load_bool(&streaming_active); } + + void obs_frontend_recording_start(void) override { QMetaObject::invokeMethod(main, "StartRecording"); } + + void obs_frontend_recording_stop(void) override { QMetaObject::invokeMethod(main, "StopRecording"); } + + bool obs_frontend_recording_active(void) override { return os_atomic_load_bool(&recording_active); } + + void obs_frontend_recording_pause(bool pause) override + { + QMetaObject::invokeMethod(main, pause ? "PauseRecording" : "UnpauseRecording"); + } + + bool obs_frontend_recording_paused(void) override { return os_atomic_load_bool(&recording_paused); } + + bool obs_frontend_recording_split_file(void) override + { + if (os_atomic_load_bool(&recording_active) && !os_atomic_load_bool(&recording_paused)) { + proc_handler_t *ph = obs_output_get_proc_handler(main->outputHandler->fileOutput); + uint8_t stack[128]; + calldata cd; + calldata_init_fixed(&cd, stack, sizeof(stack)); + proc_handler_call(ph, "split_file", &cd); + bool result = calldata_bool(&cd, "split_file_enabled"); + return result; + } else { + return false; + } + } + + bool obs_frontend_recording_add_chapter(const char *name) override + { + if (!os_atomic_load_bool(&recording_active) || os_atomic_load_bool(&recording_paused)) + return false; + + proc_handler_t *ph = obs_output_get_proc_handler(main->outputHandler->fileOutput); + + calldata cd; + calldata_init(&cd); + calldata_set_string(&cd, "chapter_name", name); + bool result = proc_handler_call(ph, "add_chapter", &cd); + calldata_free(&cd); + return result; + } + + void obs_frontend_replay_buffer_start(void) override { QMetaObject::invokeMethod(main, "StartReplayBuffer"); } + + void obs_frontend_replay_buffer_save(void) override { QMetaObject::invokeMethod(main, "ReplayBufferSave"); } + + void obs_frontend_replay_buffer_stop(void) override { QMetaObject::invokeMethod(main, "StopReplayBuffer"); } + + bool obs_frontend_replay_buffer_active(void) override { return os_atomic_load_bool(&replaybuf_active); } + + void *obs_frontend_add_tools_menu_qaction(const char *name) override + { + main->ui->menuTools->setEnabled(true); + return (void *)main->ui->menuTools->addAction(QT_UTF8(name)); + } + + void obs_frontend_add_tools_menu_item(const char *name, obs_frontend_cb callback, void *private_data) override + { + main->ui->menuTools->setEnabled(true); + + auto func = [private_data, callback]() { + callback(private_data); + }; + + QAction *action = main->ui->menuTools->addAction(QT_UTF8(name)); + QObject::connect(action, &QAction::triggered, func); + } + + void *obs_frontend_add_dock(void *dock) override + { + QDockWidget *d = reinterpret_cast(dock); + + QString name = d->objectName(); + if (name.isEmpty() || main->IsDockObjectNameUsed(name)) { + blog(LOG_WARNING, "The object name of the added dock is empty or already used," + " a temporary one will be set to avoid conflicts"); + + char *uuid = os_generate_uuid(); + name = QT_UTF8(uuid); + bfree(uuid); + name.append("_oldExtraDock"); + + d->setObjectName(name); + } + + return (void *)main->AddDockWidget(d); + } + + bool obs_frontend_add_dock_by_id(const char *id, const char *title, void *widget) override + { + if (main->IsDockObjectNameUsed(QT_UTF8(id))) { + blog(LOG_WARNING, + "Dock id '%s' already used! " + "Duplicate library?", + id); + return false; + } + + OBSDock *dock = new OBSDock(main); + dock->setWidget((QWidget *)widget); + dock->setWindowTitle(QT_UTF8(title)); + dock->setObjectName(QT_UTF8(id)); + + main->AddDockWidget(dock, Qt::RightDockWidgetArea); + + dock->setVisible(false); + dock->setFloating(true); + + return true; + } + + void obs_frontend_remove_dock(const char *id) override { main->RemoveDockWidget(QT_UTF8(id)); } + + bool obs_frontend_add_custom_qdock(const char *id, void *dock) override + { + if (main->IsDockObjectNameUsed(QT_UTF8(id))) { + blog(LOG_WARNING, + "Dock id '%s' already used! " + "Duplicate library?", + id); + return false; + } + + QDockWidget *d = reinterpret_cast(dock); + d->setObjectName(QT_UTF8(id)); + + main->AddCustomDockWidget(d); + + return true; + } + + void obs_frontend_add_event_callback(obs_frontend_event_cb callback, void *private_data) override + { + size_t idx = GetCallbackIdx(callbacks, callback, private_data); + if (idx == (size_t)-1) + callbacks.emplace_back(callback, private_data); + } + + void obs_frontend_remove_event_callback(obs_frontend_event_cb callback, void *private_data) override + { + size_t idx = GetCallbackIdx(callbacks, callback, private_data); + if (idx == (size_t)-1) + return; + + callbacks.erase(callbacks.begin() + idx); + } + + obs_output_t *obs_frontend_get_streaming_output(void) override + { + auto multitrackVideo = main->outputHandler->multitrackVideo.get(); + auto mtvOutput = multitrackVideo ? obs_output_get_ref(multitrackVideo->StreamingOutput()) : nullptr; + if (mtvOutput) + return mtvOutput; + + OBSOutput output = main->outputHandler->streamOutput.Get(); + return obs_output_get_ref(output); + } + + obs_output_t *obs_frontend_get_recording_output(void) override + { + OBSOutput out = main->outputHandler->fileOutput.Get(); + return obs_output_get_ref(out); + } + + obs_output_t *obs_frontend_get_replay_buffer_output(void) override + { + OBSOutput out = main->outputHandler->replayBuffer.Get(); + return obs_output_get_ref(out); + } + + config_t *obs_frontend_get_profile_config(void) override { 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/frontend/obs-main.cpp b/frontend/obs-main.cpp new file mode 100644 index 000000000..883bee21a --- /dev/null +++ b/frontend/obs-main.cpp @@ -0,0 +1,2662 @@ +/****************************************************************************** + Copyright (C) 2023 by Lain Bailey + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "obs-app.hpp" +#include "obs-proxy-style.hpp" +#include "log-viewer.hpp" +#include "volume-control.hpp" +#include "window-basic-main.hpp" +#ifdef __APPLE__ +#include "window-permissions.hpp" +#endif +#include "window-basic-settings.hpp" +#include "platform.hpp" + +#include + +#include + +#ifdef _WIN32 +#include +#include +#include +#else +#include +#include +#include +#include +#include +#endif + +#if defined(_WIN32) || defined(ENABLE_SPARKLE_UPDATER) +#include "update/models/branches.hpp" +#endif + +#if !defined(_WIN32) && !defined(__APPLE__) +#include +#include +#endif + +#include + +#include "ui-config.h" + +using namespace std; + +static log_handler_t def_log_handler; + +static string currentLogFile; +static string lastLogFile; +static string lastCrashLogFile; + +bool portable_mode = false; +bool steam = false; +bool safe_mode = false; +bool disable_3p_plugins = false; +bool unclean_shutdown = false; +bool disable_shutdown_check = false; +static bool multi = false; +static bool log_verbose = false; +static bool unfiltered_log = false; +bool opt_start_streaming = false; +bool opt_start_recording = false; +bool opt_studio_mode = false; +bool opt_start_replaybuffer = false; +bool opt_start_virtualcam = false; +bool opt_minimize_tray = false; +bool opt_allow_opengl = false; +bool opt_always_on_top = false; +bool opt_disable_updater = false; +bool opt_disable_missing_files_check = false; +string opt_starting_collection; +string opt_starting_profile; +string opt_starting_scene; + +bool restart = false; +bool restart_safe = false; +QStringList arguments; + +QPointer obsLogViewer; + +#ifndef _WIN32 +int OBSApp::sigintFd[2]; +#endif + +// GPU hint exports for AMD/NVIDIA laptops +#ifdef _MSC_VER +extern "C" __declspec(dllexport) DWORD NvOptimusEnablement = 1; +extern "C" __declspec(dllexport) int AmdPowerXpressRequestHighPerformance = 1; +#endif + +QObject *CreateShortcutFilter() +{ + return new OBSEventFilter([](QObject *obj, QEvent *event) { + auto mouse_event = [](QMouseEvent &event) { + if (!App()->HotkeysEnabledInFocus() && event.button() != Qt::LeftButton) + return true; + + obs_key_combination_t hotkey = {0, OBS_KEY_NONE}; + bool pressed = event.type() == QEvent::MouseButtonPress; + + switch (event.button()) { + case Qt::NoButton: + case Qt::LeftButton: + case Qt::RightButton: + case Qt::AllButtons: + case Qt::MouseButtonMask: + return false; + + case Qt::MiddleButton: + hotkey.key = OBS_KEY_MOUSE3; + break; + +#define MAP_BUTTON(i, j) \ + case Qt::ExtraButton##i: \ + hotkey.key = OBS_KEY_MOUSE##j; \ + break; + MAP_BUTTON(1, 4); + MAP_BUTTON(2, 5); + MAP_BUTTON(3, 6); + MAP_BUTTON(4, 7); + MAP_BUTTON(5, 8); + MAP_BUTTON(6, 9); + MAP_BUTTON(7, 10); + MAP_BUTTON(8, 11); + MAP_BUTTON(9, 12); + MAP_BUTTON(10, 13); + MAP_BUTTON(11, 14); + MAP_BUTTON(12, 15); + MAP_BUTTON(13, 16); + MAP_BUTTON(14, 17); + MAP_BUTTON(15, 18); + MAP_BUTTON(16, 19); + MAP_BUTTON(17, 20); + MAP_BUTTON(18, 21); + MAP_BUTTON(19, 22); + MAP_BUTTON(20, 23); + MAP_BUTTON(21, 24); + MAP_BUTTON(22, 25); + MAP_BUTTON(23, 26); + MAP_BUTTON(24, 27); +#undef MAP_BUTTON + } + + hotkey.modifiers = TranslateQtKeyboardEventModifiers(event.modifiers()); + + obs_hotkey_inject_event(hotkey, pressed); + return true; + }; + + auto key_event = [&](QKeyEvent *event) { + int key = event->key(); + bool enabledInFocus = App()->HotkeysEnabledInFocus(); + + if (key != Qt::Key_Enter && key != Qt::Key_Escape && key != Qt::Key_Return && !enabledInFocus) + return true; + + QDialog *dialog = qobject_cast(obj); + + obs_key_combination_t hotkey = {0, OBS_KEY_NONE}; + bool pressed = event->type() == QEvent::KeyPress; + + switch (key) { + case Qt::Key_Shift: + case Qt::Key_Control: + case Qt::Key_Alt: + case Qt::Key_Meta: + break; + +#ifdef __APPLE__ + case Qt::Key_CapsLock: + // kVK_CapsLock == 57 + hotkey.key = obs_key_from_virtual_key(57); + pressed = true; + break; +#endif + + case Qt::Key_Enter: + case Qt::Key_Escape: + case Qt::Key_Return: + if (dialog && pressed) + return false; + if (!enabledInFocus) + return true; + /* Falls through. */ + default: + hotkey.key = obs_key_from_virtual_key(event->nativeVirtualKey()); + } + + if (event->isAutoRepeat()) + return true; + + hotkey.modifiers = TranslateQtKeyboardEventModifiers(event->modifiers()); + + obs_hotkey_inject_event(hotkey, pressed); + return true; + }; + + switch (event->type()) { + case QEvent::MouseButtonPress: + case QEvent::MouseButtonRelease: + return mouse_event(*static_cast(event)); + + /*case QEvent::MouseButtonDblClick: + case QEvent::Wheel:*/ + case QEvent::KeyPress: + case QEvent::KeyRelease: + return key_event(static_cast(event)); + + default: + return false; + } + }); +} + +string CurrentTimeString() +{ + using namespace std::chrono; + + struct tm tstruct; + char buf[80]; + + auto tp = system_clock::now(); + auto now = system_clock::to_time_t(tp); + tstruct = *localtime(&now); + + size_t written = strftime(buf, sizeof(buf), "%T", &tstruct); + if (ratio_less::value && written && (sizeof(buf) - written) > 5) { + auto tp_secs = time_point_cast(tp); + auto millis = duration_cast(tp - tp_secs).count(); + + snprintf(buf + written, sizeof(buf) - written, ".%03u", static_cast(millis)); + } + + return buf; +} + +string CurrentDateTimeString() +{ + time_t now = time(0); + struct tm tstruct; + char buf[80]; + tstruct = *localtime(&now); + strftime(buf, sizeof(buf), "%Y-%m-%d, %X", &tstruct); + return buf; +} + +static void LogString(fstream &logFile, const char *timeString, char *str, int log_level) +{ + static mutex logfile_mutex; + string msg; + msg += timeString; + msg += str; + + logfile_mutex.lock(); + logFile << msg << endl; + logfile_mutex.unlock(); + + if (!!obsLogViewer) + QMetaObject::invokeMethod(obsLogViewer.data(), "AddLine", Qt::QueuedConnection, Q_ARG(int, log_level), + Q_ARG(QString, QString(msg.c_str()))); +} + +static inline void LogStringChunk(fstream &logFile, char *str, int log_level) +{ + char *nextLine = str; + string timeString = CurrentTimeString(); + timeString += ": "; + + while (*nextLine) { + char *nextLine = strchr(str, '\n'); + if (!nextLine) + break; + + if (nextLine != str && nextLine[-1] == '\r') { + nextLine[-1] = 0; + } else { + nextLine[0] = 0; + } + + LogString(logFile, timeString.c_str(), str, log_level); + nextLine++; + str = nextLine; + } + + LogString(logFile, timeString.c_str(), str, log_level); +} + +#define MAX_REPEATED_LINES 30 +#define MAX_CHAR_VARIATION (255 * 3) + +static inline int sum_chars(const char *str) +{ + int val = 0; + for (; *str != 0; str++) + val += *str; + + return val; +} + +static inline bool too_many_repeated_entries(fstream &logFile, const char *msg, const char *output_str) +{ + static mutex log_mutex; + static const char *last_msg_ptr = nullptr; + static int last_char_sum = 0; + static int rep_count = 0; + + int new_sum = sum_chars(output_str); + + lock_guard guard(log_mutex); + + if (unfiltered_log) { + return false; + } + + if (last_msg_ptr == msg) { + int diff = std::abs(new_sum - last_char_sum); + if (diff < MAX_CHAR_VARIATION) { + return (rep_count++ >= MAX_REPEATED_LINES); + } + } + + if (rep_count > MAX_REPEATED_LINES) { + logFile << CurrentTimeString() << ": Last log entry repeated for " + << to_string(rep_count - MAX_REPEATED_LINES) << " more lines" << endl; + } + + last_msg_ptr = msg; + last_char_sum = new_sum; + rep_count = 0; + + return false; +} + +static void do_log(int log_level, const char *msg, va_list args, void *param) +{ + fstream &logFile = *static_cast(param); + char str[8192]; + +#ifndef _WIN32 + va_list args2; + va_copy(args2, args); +#endif + + vsnprintf(str, sizeof(str), msg, args); + +#ifdef _WIN32 + if (IsDebuggerPresent()) { + int wNum = MultiByteToWideChar(CP_UTF8, 0, str, -1, NULL, 0); + if (wNum > 1) { + static wstring wide_buf; + static mutex wide_mutex; + + lock_guard lock(wide_mutex); + wide_buf.reserve(wNum + 1); + wide_buf.resize(wNum - 1); + MultiByteToWideChar(CP_UTF8, 0, str, -1, &wide_buf[0], wNum); + wide_buf.push_back('\n'); + + OutputDebugStringW(wide_buf.c_str()); + } + } +#endif + +#if !defined(_WIN32) && defined(_DEBUG) + def_log_handler(log_level, msg, args2, nullptr); +#endif + + if (log_level <= LOG_INFO || log_verbose) { +#if !defined(_WIN32) && !defined(_DEBUG) + def_log_handler(log_level, msg, args2, nullptr); +#endif + if (!too_many_repeated_entries(logFile, msg, str)) + LogStringChunk(logFile, str, log_level); + } + +#if defined(_WIN32) && defined(OBS_DEBUGBREAK_ON_ERROR) + if (log_level <= LOG_ERROR && IsDebuggerPresent()) + __debugbreak(); +#endif + +#ifndef _WIN32 + va_end(args2); +#endif +} + +#define DEFAULT_LANG "en-US" + +bool OBSApp::InitGlobalConfigDefaults() +{ + config_set_default_uint(appConfig, "General", "MaxLogs", 10); + config_set_default_int(appConfig, "General", "InfoIncrement", -1); + config_set_default_string(appConfig, "General", "ProcessPriority", "Normal"); + config_set_default_bool(appConfig, "General", "EnableAutoUpdates", true); + +#if _WIN32 + config_set_default_string(appConfig, "Video", "Renderer", "Direct3D 11"); +#else + config_set_default_string(appConfig, "Video", "Renderer", "OpenGL"); +#endif + +#ifdef _WIN32 + config_set_default_bool(appConfig, "Audio", "DisableAudioDucking", true); + config_set_default_bool(appConfig, "General", "BrowserHWAccel", true); +#endif + +#ifdef __APPLE__ + config_set_default_bool(appConfig, "General", "BrowserHWAccel", true); + config_set_default_bool(appConfig, "Video", "DisableOSXVSync", true); + config_set_default_bool(appConfig, "Video", "ResetOSXVSyncOnExit", true); +#endif + + return true; +} + +bool OBSApp::InitGlobalLocationDefaults() +{ + char path[512]; + + int len = GetAppConfigPath(path, sizeof(path), nullptr); + if (len <= 0) { + OBSErrorBox(NULL, "Unable to get global configuration path."); + return false; + } + + config_set_default_string(appConfig, "Locations", "Configuration", path); + config_set_default_string(appConfig, "Locations", "SceneCollections", path); + config_set_default_string(appConfig, "Locations", "Profiles", path); + + return true; +} + +void OBSApp::InitUserConfigDefaults() +{ + config_set_default_bool(userConfig, "General", "ConfirmOnExit", true); + + config_set_default_string(userConfig, "General", "HotkeyFocusType", "NeverDisableHotkeys"); + + config_set_default_bool(userConfig, "BasicWindow", "PreviewEnabled", true); + config_set_default_bool(userConfig, "BasicWindow", "PreviewProgramMode", false); + config_set_default_bool(userConfig, "BasicWindow", "SceneDuplicationMode", true); + config_set_default_bool(userConfig, "BasicWindow", "SwapScenesMode", true); + config_set_default_bool(userConfig, "BasicWindow", "SnappingEnabled", true); + config_set_default_bool(userConfig, "BasicWindow", "ScreenSnapping", true); + config_set_default_bool(userConfig, "BasicWindow", "SourceSnapping", true); + config_set_default_bool(userConfig, "BasicWindow", "CenterSnapping", false); + config_set_default_double(userConfig, "BasicWindow", "SnapDistance", 10.0); + config_set_default_bool(userConfig, "BasicWindow", "SpacingHelpersEnabled", true); + config_set_default_bool(userConfig, "BasicWindow", "RecordWhenStreaming", false); + config_set_default_bool(userConfig, "BasicWindow", "KeepRecordingWhenStreamStops", false); + config_set_default_bool(userConfig, "BasicWindow", "SysTrayEnabled", true); + config_set_default_bool(userConfig, "BasicWindow", "SysTrayWhenStarted", false); + config_set_default_bool(userConfig, "BasicWindow", "SaveProjectors", false); + config_set_default_bool(userConfig, "BasicWindow", "ShowTransitions", true); + config_set_default_bool(userConfig, "BasicWindow", "ShowListboxToolbars", true); + config_set_default_bool(userConfig, "BasicWindow", "ShowStatusBar", true); + config_set_default_bool(userConfig, "BasicWindow", "ShowSourceIcons", true); + config_set_default_bool(userConfig, "BasicWindow", "ShowContextToolbars", true); + config_set_default_bool(userConfig, "BasicWindow", "StudioModeLabels", true); + + config_set_default_bool(userConfig, "BasicWindow", "VerticalVolControl", false); + + config_set_default_bool(userConfig, "BasicWindow", "MultiviewMouseSwitch", true); + + config_set_default_bool(userConfig, "BasicWindow", "MultiviewDrawNames", true); + + config_set_default_bool(userConfig, "BasicWindow", "MultiviewDrawAreas", true); + + config_set_default_bool(userConfig, "BasicWindow", "MediaControlsCountdownTimer", true); +} + +static bool do_mkdir(const char *path) +{ + if (os_mkdirs(path) == MKDIR_ERROR) { + OBSErrorBox(NULL, "Failed to create directory %s", path); + return false; + } + + return true; +} + +static bool MakeUserDirs() +{ + char path[512]; + + if (GetAppConfigPath(path, sizeof(path), "obs-studio/basic") <= 0) + return false; + if (!do_mkdir(path)) + return false; + + if (GetAppConfigPath(path, sizeof(path), "obs-studio/logs") <= 0) + return false; + if (!do_mkdir(path)) + return false; + + if (GetAppConfigPath(path, sizeof(path), "obs-studio/profiler_data") <= 0) + return false; + if (!do_mkdir(path)) + return false; + +#ifdef _WIN32 + if (GetAppConfigPath(path, sizeof(path), "obs-studio/crashes") <= 0) + return false; + if (!do_mkdir(path)) + return false; +#endif + +#ifdef WHATSNEW_ENABLED + if (GetAppConfigPath(path, sizeof(path), "obs-studio/updates") <= 0) + return false; + if (!do_mkdir(path)) + return false; +#endif + + if (GetAppConfigPath(path, sizeof(path), "obs-studio/plugin_config") <= 0) + return false; + if (!do_mkdir(path)) + return false; + + return true; +} + +constexpr std::string_view OBSProfileSubDirectory = "obs-studio/basic/profiles"; +constexpr std::string_view OBSScenesSubDirectory = "obs-studio/basic/scenes"; + +static bool MakeUserProfileDirs() +{ + const std::filesystem::path userProfilePath = + App()->userProfilesLocation / std::filesystem::u8path(OBSProfileSubDirectory); + const std::filesystem::path userScenesPath = + App()->userScenesLocation / std::filesystem::u8path(OBSScenesSubDirectory); + + 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 (!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; +} + +bool OBSApp::UpdatePre22MultiviewLayout(const char *layout) +{ + if (!layout) + return false; + + if (astrcmpi(layout, "horizontaltop") == 0) { + config_set_int(userConfig, "BasicWindow", "MultiviewLayout", + static_cast(MultiviewLayout::HORIZONTAL_TOP_8_SCENES)); + return true; + } + + if (astrcmpi(layout, "horizontalbottom") == 0) { + config_set_int(userConfig, "BasicWindow", "MultiviewLayout", + static_cast(MultiviewLayout::HORIZONTAL_BOTTOM_8_SCENES)); + return true; + } + + if (astrcmpi(layout, "verticalleft") == 0) { + config_set_int(userConfig, "BasicWindow", "MultiviewLayout", + static_cast(MultiviewLayout::VERTICAL_LEFT_8_SCENES)); + return true; + } + + if (astrcmpi(layout, "verticalright") == 0) { + config_set_int(userConfig, "BasicWindow", "MultiviewLayout", + static_cast(MultiviewLayout::VERTICAL_RIGHT_8_SCENES)); + return true; + } + + return false; +} + +bool OBSApp::InitGlobalConfig() +{ + char path[512]; + + int len = GetAppConfigPath(path, sizeof(path), "obs-studio/global.ini"); + if (len <= 0) { + return false; + } + + int errorcode = appConfig.Open(path, CONFIG_OPEN_ALWAYS); + if (errorcode != CONFIG_SUCCESS) { + OBSErrorBox(NULL, "Failed to open global.ini: %d", errorcode); + return false; + } + + uint32_t lastVersion = config_get_int(appConfig, "General", "LastVersion"); + + if (lastVersion < MAKE_SEMANTIC_VERSION(31, 0, 0)) { + bool migratedUserSettings = config_get_bool(appConfig, "General", "Pre31Migrated"); + + if (!migratedUserSettings) { + bool migrated = MigrateGlobalSettings(); + + config_set_bool(appConfig, "General", "Pre31Migrated", migrated); + config_save_safe(appConfig, "tmp", nullptr); + } + } + + InitGlobalConfigDefaults(); + InitGlobalLocationDefaults(); + + if (IsPortableMode()) { + userConfigLocation = + std::filesystem::u8path(config_get_default_string(appConfig, "Locations", "Configuration")); + userScenesLocation = + std::filesystem::u8path(config_get_default_string(appConfig, "Locations", "SceneCollections")); + userProfilesLocation = + std::filesystem::u8path(config_get_default_string(appConfig, "Locations", "Profiles")); + } else { + userConfigLocation = + std::filesystem::u8path(config_get_string(appConfig, "Locations", "Configuration")); + userScenesLocation = + std::filesystem::u8path(config_get_string(appConfig, "Locations", "SceneCollections")); + userProfilesLocation = std::filesystem::u8path(config_get_string(appConfig, "Locations", "Profiles")); + } + + bool userConfigResult = InitUserConfig(userConfigLocation, lastVersion); + + return userConfigResult; +} + +bool OBSApp::InitUserConfig(std::filesystem::path &userConfigLocation, uint32_t lastVersion) +{ + const std::string userConfigFile = userConfigLocation.u8string() + "/obs-studio/user.ini"; + + int errorCode = userConfig.Open(userConfigFile.c_str(), CONFIG_OPEN_ALWAYS); + + if (errorCode != CONFIG_SUCCESS) { + OBSErrorBox(nullptr, "Failed to open user.ini: %d", errorCode); + return false; + } + + MigrateLegacySettings(lastVersion); + InitUserConfigDefaults(); + + return true; +} + +void OBSApp::MigrateLegacySettings(const uint32_t lastVersion) +{ + bool hasChanges = false; + + const uint32_t v19 = MAKE_SEMANTIC_VERSION(19, 0, 0); + const uint32_t v21 = MAKE_SEMANTIC_VERSION(21, 0, 0); + const uint32_t v23 = MAKE_SEMANTIC_VERSION(23, 0, 0); + const uint32_t v24 = MAKE_SEMANTIC_VERSION(24, 0, 0); + const uint32_t v24_1 = MAKE_SEMANTIC_VERSION(24, 1, 0); + + const map defaultsMap{ + {{v19, "Pre19Defaults"}, {v21, "Pre21Defaults"}, {v23, "Pre23Defaults"}, {v24_1, "Pre24.1Defaults"}}}; + + for (auto &[version, configKey] : defaultsMap) { + if (!config_has_user_value(userConfig, "General", configKey.c_str())) { + bool useOldDefaults = lastVersion && lastVersion < version; + config_set_bool(userConfig, "General", configKey.c_str(), useOldDefaults); + + hasChanges = true; + } + } + + if (config_has_user_value(userConfig, "BasicWindow", "MultiviewLayout")) { + const char *layout = config_get_string(userConfig, "BasicWindow", "MultiviewLayout"); + + bool layoutUpdated = UpdatePre22MultiviewLayout(layout); + + hasChanges = hasChanges | layoutUpdated; + } + + if (lastVersion && lastVersion < v24) { + bool disableHotkeysInFocus = config_get_bool(userConfig, "General", "DisableHotkeysInFocus"); + + if (disableHotkeysInFocus) { + config_set_string(userConfig, "General", "HotkeyFocusType", "DisableHotkeysInFocus"); + } + + hasChanges = true; + } + + if (hasChanges) { + userConfig.SaveSafe("tmp"); + } +} + +static constexpr string_view OBSGlobalIniPath = "/obs-studio/global.ini"; +static constexpr string_view OBSUserIniPath = "/obs-studio/user.ini"; + +bool OBSApp::MigrateGlobalSettings() +{ + char path[512]; + + int len = GetAppConfigPath(path, sizeof(path), nullptr); + if (len <= 0) { + OBSErrorBox(nullptr, "Unable to get global configuration path."); + return false; + } + + std::string legacyConfigFileString; + legacyConfigFileString.reserve(strlen(path) + OBSGlobalIniPath.size()); + legacyConfigFileString.append(path).append(OBSGlobalIniPath); + + const std::filesystem::path legacyGlobalConfigFile = std::filesystem::u8path(legacyConfigFileString); + + std::string configFileString; + configFileString.reserve(strlen(path) + OBSUserIniPath.size()); + configFileString.append(path).append(OBSUserIniPath); + + const std::filesystem::path userConfigFile = std::filesystem::u8path(configFileString); + + if (std::filesystem::exists(userConfigFile)) { + OBSErrorBox(nullptr, + "Unable to migrate global configuration - user configuration file already exists."); + return false; + } + + try { + std::filesystem::copy(legacyGlobalConfigFile, userConfigFile); + } catch (const std::filesystem::filesystem_error &) { + OBSErrorBox(nullptr, "Unable to migrate global configuration - copy failed."); + return false; + } + + return true; +} + +bool OBSApp::InitLocale() +{ + ProfileScope("OBSApp::InitLocale"); + + const char *lang = config_get_string(userConfig, "General", "Language"); + bool userLocale = config_has_user_value(userConfig, "General", "Language"); + if (!userLocale || !lang || lang[0] == '\0') + lang = DEFAULT_LANG; + + locale = lang; + + // set basic default application locale + if (!locale.empty()) + QLocale::setDefault(QLocale(QString::fromStdString(locale).replace('-', '_'))); + + string englishPath; + if (!GetDataFilePath("locale/" DEFAULT_LANG ".ini", englishPath)) { + OBSErrorBox(NULL, "Failed to find locale/" DEFAULT_LANG ".ini"); + return false; + } + + textLookup = text_lookup_create(englishPath.c_str()); + if (!textLookup) { + OBSErrorBox(NULL, "Failed to create locale from file '%s'", englishPath.c_str()); + return false; + } + + bool defaultLang = astrcmpi(lang, DEFAULT_LANG) == 0; + + if (userLocale && defaultLang) + return true; + + if (!userLocale && defaultLang) { + for (auto &locale_ : GetPreferredLocales()) { + if (locale_ == lang) + return true; + + stringstream file; + file << "locale/" << locale_ << ".ini"; + + string path; + if (!GetDataFilePath(file.str().c_str(), path)) + continue; + + if (!text_lookup_add(textLookup, path.c_str())) + continue; + + blog(LOG_INFO, "Using preferred locale '%s'", locale_.c_str()); + locale = locale_; + + // set application default locale to the new choosen one + if (!locale.empty()) + QLocale::setDefault(QLocale(QString::fromStdString(locale).replace('-', '_'))); + + return true; + } + + return true; + } + + stringstream file; + file << "locale/" << lang << ".ini"; + + string path; + if (GetDataFilePath(file.str().c_str(), path)) { + if (!text_lookup_add(textLookup, path.c_str())) + blog(LOG_ERROR, "Failed to add locale file '%s'", path.c_str()); + } else { + blog(LOG_ERROR, "Could not find locale file '%s'", file.str().c_str()); + } + + return true; +} + +#if defined(_WIN32) || defined(ENABLE_SPARKLE_UPDATER) +void ParseBranchesJson(const std::string &jsonString, vector &out, std::string &error) +{ + JsonBranches branches; + + try { + nlohmann::json json = nlohmann::json::parse(jsonString); + branches = json.get(); + } catch (nlohmann::json::exception &e) { + error = e.what(); + return; + } + + for (const JsonBranch &json_branch : branches) { +#ifdef _WIN32 + if (!json_branch.windows) + continue; +#elif defined(__APPLE__) + if (!json_branch.macos) + continue; +#endif + + UpdateBranch branch = { + QString::fromStdString(json_branch.name), + QString::fromStdString(json_branch.display_name), + QString::fromStdString(json_branch.description), + json_branch.enabled, + json_branch.visible, + }; + out.push_back(branch); + } +} + +bool LoadBranchesFile(vector &out) +{ + string error; + string branchesText; + + BPtr branchesFilePath = GetAppConfigPathPtr("obs-studio/updates/branches.json"); + + QFile branchesFile(branchesFilePath.Get()); + if (!branchesFile.open(QIODevice::ReadOnly)) { + error = "Opening file failed."; + goto fail; + } + + branchesText = branchesFile.readAll(); + if (branchesText.empty()) { + error = "File empty."; + goto fail; + } + + ParseBranchesJson(branchesText, out, error); + if (error.empty()) + return !out.empty(); + +fail: + blog(LOG_WARNING, "Loading branches from file failed: %s", error.c_str()); + return false; +} +#endif + +void OBSApp::SetBranchData(const string &data) +{ +#if defined(_WIN32) || defined(ENABLE_SPARKLE_UPDATER) + string error; + vector result; + + ParseBranchesJson(data, result, error); + + if (!error.empty()) { + blog(LOG_WARNING, "Reading branches JSON response failed: %s", error.c_str()); + return; + } + + if (!result.empty()) + updateBranches = result; + + branches_loaded = true; +#else + UNUSED_PARAMETER(data); +#endif +} + +std::vector OBSApp::GetBranches() +{ + vector out; + /* Always ensure the default branch exists */ + out.push_back(UpdateBranch{"stable", "", "", true, true}); + +#if defined(_WIN32) || defined(ENABLE_SPARKLE_UPDATER) + if (!branches_loaded) { + vector result; + if (LoadBranchesFile(result)) + updateBranches = result; + + branches_loaded = true; + } +#endif + + /* Copy additional branches to result (if any) */ + if (!updateBranches.empty()) + out.insert(out.end(), updateBranches.begin(), updateBranches.end()); + + return out; +} + +OBSApp::OBSApp(int &argc, char **argv, profiler_name_store_t *store) + : QApplication(argc, argv), + profilerNameStore(store) +{ + /* fix float handling */ +#if defined(Q_OS_UNIX) + if (!setlocale(LC_NUMERIC, "C")) + blog(LOG_WARNING, "Failed to set LC_NUMERIC to C locale"); +#endif + +#ifndef _WIN32 + /* Handle SIGINT properly */ + socketpair(AF_UNIX, SOCK_STREAM, 0, sigintFd); + snInt = new QSocketNotifier(sigintFd[1], QSocketNotifier::Read, this); + connect(snInt, &QSocketNotifier::activated, this, &OBSApp::ProcessSigInt); +#else + connect(qApp, &QGuiApplication::commitDataRequest, this, &OBSApp::commitData); +#endif + + sleepInhibitor = os_inhibit_sleep_create("OBS Video/audio"); + +#ifndef __APPLE__ + setWindowIcon(QIcon::fromTheme("obs", QIcon(":/res/images/obs.png"))); +#endif + + setDesktopFileName("com.obsproject.Studio"); +} + +OBSApp::~OBSApp() +{ +#ifdef _WIN32 + bool disableAudioDucking = config_get_bool(appConfig, "Audio", "DisableAudioDucking"); + if (disableAudioDucking) + DisableAudioDucking(false); +#else + delete snInt; + close(sigintFd[0]); + close(sigintFd[1]); +#endif + +#ifdef __APPLE__ + bool vsyncDisabled = config_get_bool(appConfig, "Video", "DisableOSXVSync"); + bool resetVSync = config_get_bool(appConfig, "Video", "ResetOSXVSyncOnExit"); + if (vsyncDisabled && resetVSync) + EnableOSXVSync(true); +#endif + + os_inhibit_sleep_set_active(sleepInhibitor, false); + os_inhibit_sleep_destroy(sleepInhibitor); + + if (libobs_initialized) + obs_shutdown(); +} + +static void move_basic_to_profiles(void) +{ + char path[512]; + + if (GetAppConfigPath(path, 512, "obs-studio/basic") <= 0) { + return; + } + + 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) +{ + char path[512]; + + if (GetAppConfigPath(path, 512, "obs-studio/basic") <= 0) { + return; + } + + const std::filesystem::path basicPath = std::filesystem::u8path(path); + + if (!std::filesystem::exists(basicPath)) { + return; + } + + const std::filesystem::path sceneCollectionPath = + App()->userScenesLocation / std::filesystem::u8path("obs-studio/basic/scenes"); + + if (std::filesystem::exists(sceneCollectionPath)) { + return; + } + + try { + std::filesystem::create_directories(sceneCollectionPath); + } catch (const std::filesystem::filesystem_error &error) { + blog(LOG_ERROR, + "Failed to create scene collection directory for migration from basic scene collection\n%s", + error.what()); + return; + } + + const std::filesystem::path sourceFile = basicPath / std::filesystem::u8path("scenes.json"); + const std::filesystem::path destinationFile = + (sceneCollectionPath / std::filesystem::u8path(Str("Untitled"))).replace_extension(".json"); + + try { + std::filesystem::rename(sourceFile, destinationFile); + } catch (const std::filesystem::filesystem_error &error) { + blog(LOG_ERROR, "Failed to rename basic scene collection file:\n%s", error.what()); + return; + } +} + +void OBSApp::AppInit() +{ + ProfileScope("OBSApp::AppInit"); + + if (!MakeUserDirs()) + throw "Failed to create required user directories"; + if (!InitGlobalConfig()) + throw "Failed to initialize global config"; + if (!InitLocale()) + throw "Failed to load locale"; + if (!InitTheme()) + throw "Failed to load theme"; + + config_set_default_string(userConfig, "Basic", "Profile", Str("Untitled")); + config_set_default_string(userConfig, "Basic", "ProfileDir", Str("Untitled")); + config_set_default_string(userConfig, "Basic", "SceneCollection", Str("Untitled")); + config_set_default_string(userConfig, "Basic", "SceneCollectionFile", Str("Untitled")); + config_set_default_bool(userConfig, "Basic", "ConfigOnNewProfile", true); + + if (!config_has_user_value(userConfig, "Basic", "Profile")) { + config_set_string(userConfig, "Basic", "Profile", Str("Untitled")); + config_set_string(userConfig, "Basic", "ProfileDir", Str("Untitled")); + } + + if (!config_has_user_value(userConfig, "Basic", "SceneCollection")) { + config_set_string(userConfig, "Basic", "SceneCollection", Str("Untitled")); + config_set_string(userConfig, "Basic", "SceneCollectionFile", Str("Untitled")); + } + +#ifdef _WIN32 + bool disableAudioDucking = config_get_bool(appConfig, "Audio", "DisableAudioDucking"); + if (disableAudioDucking) + DisableAudioDucking(true); +#endif + +#ifdef __APPLE__ + if (config_get_bool(appConfig, "Video", "DisableOSXVSync")) + EnableOSXVSync(false); +#endif + + UpdateHotkeyFocusSetting(false); + + move_basic_to_profiles(); + move_basic_to_scene_collections(); + + if (!MakeUserProfileDirs()) + throw "Failed to create profile directories"; +} + +const char *OBSApp::GetRenderModule() const +{ + const char *renderer = config_get_string(appConfig, "Video", "Renderer"); + + return (astrcmpi(renderer, "Direct3D 11") == 0) ? DL_D3D11 : DL_OPENGL; +} + +static bool StartupOBS(const char *locale, profiler_name_store_t *store) +{ + char path[512]; + + if (GetAppConfigPath(path, sizeof(path), "obs-studio/plugin_config") <= 0) + return false; + + return obs_startup(locale, path, store); +} + +inline void OBSApp::ResetHotkeyState(bool inFocus) +{ + obs_hotkey_enable_background_press((inFocus && enableHotkeysInFocus) || (!inFocus && enableHotkeysOutOfFocus)); +} + +void OBSApp::UpdateHotkeyFocusSetting(bool resetState) +{ + enableHotkeysInFocus = true; + enableHotkeysOutOfFocus = true; + + const char *hotkeyFocusType = config_get_string(userConfig, "General", "HotkeyFocusType"); + + if (astrcmpi(hotkeyFocusType, "DisableHotkeysInFocus") == 0) { + enableHotkeysInFocus = false; + } else if (astrcmpi(hotkeyFocusType, "DisableHotkeysOutOfFocus") == 0) { + enableHotkeysOutOfFocus = false; + } + + if (resetState) + ResetHotkeyState(applicationState() == Qt::ApplicationActive); +} + +void OBSApp::DisableHotkeys() +{ + enableHotkeysInFocus = false; + enableHotkeysOutOfFocus = false; + ResetHotkeyState(applicationState() == Qt::ApplicationActive); +} + +Q_DECLARE_METATYPE(VoidFunc) + +void OBSApp::Exec(VoidFunc func) +{ + func(); +} + +static void ui_task_handler(obs_task_t task, void *param, bool wait) +{ + auto doTask = [=]() { + /* to get clang-format to behave */ + task(param); + }; + QMetaObject::invokeMethod(App(), "Exec", wait ? WaitConnection() : Qt::AutoConnection, Q_ARG(VoidFunc, doTask)); +} + +bool OBSApp::OBSInit() +{ + ProfileScope("OBSApp::OBSInit"); + + qRegisterMetaType("VoidFunc"); + +#if !defined(_WIN32) && !defined(__APPLE__) + if (QApplication::platformName() == "xcb") { + obs_set_nix_platform(OBS_NIX_PLATFORM_X11_EGL); + blog(LOG_INFO, "Using EGL/X11"); + } + +#ifdef ENABLE_WAYLAND + if (QApplication::platformName().contains("wayland")) { + obs_set_nix_platform(OBS_NIX_PLATFORM_WAYLAND); + setAttribute(Qt::AA_DontCreateNativeWidgetSiblings); + blog(LOG_INFO, "Platform: Wayland"); + } +#endif + + QPlatformNativeInterface *native = QGuiApplication::platformNativeInterface(); + obs_set_nix_platform_display(native->nativeResourceForIntegration("display")); +#endif + +#ifdef __APPLE__ + setAttribute(Qt::AA_DontCreateNativeWidgetSiblings); +#endif + + if (!StartupOBS(locale.c_str(), GetProfilerNameStore())) + return false; + + libobs_initialized = true; + + obs_set_ui_task_handler(ui_task_handler); + +#if defined(_WIN32) || defined(__APPLE__) + bool browserHWAccel = config_get_bool(appConfig, "General", "BrowserHWAccel"); + + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_bool(settings, "BrowserHWAccel", browserHWAccel); + obs_apply_private_data(settings); + + blog(LOG_INFO, "Current Date/Time: %s", CurrentDateTimeString().c_str()); + + blog(LOG_INFO, "Browser Hardware Acceleration: %s", browserHWAccel ? "true" : "false"); +#endif +#ifdef _WIN32 + bool hideFromCapture = config_get_bool(userConfig, "BasicWindow", "HideOBSWindowsFromCapture"); + blog(LOG_INFO, "Hide OBS windows from screen capture: %s", hideFromCapture ? "true" : "false"); +#endif + + blog(LOG_INFO, "Qt Version: %s (runtime), %s (compiled)", qVersion(), QT_VERSION_STR); + blog(LOG_INFO, "Portable mode: %s", portable_mode ? "true" : "false"); + + if (safe_mode) { + blog(LOG_WARNING, "Safe Mode enabled."); + } else if (disable_3p_plugins) { + blog(LOG_WARNING, "Third-party plugins disabled."); + } + + setQuitOnLastWindowClosed(false); + + mainWindow = new OBSBasic(); + + mainWindow->setAttribute(Qt::WA_DeleteOnClose, true); + connect(mainWindow, &OBSBasic::destroyed, this, &OBSApp::quit); + + mainWindow->OBSInit(); + + connect(this, &QGuiApplication::applicationStateChanged, + [this](Qt::ApplicationState state) { ResetHotkeyState(state == Qt::ApplicationActive); }); + ResetHotkeyState(applicationState() == Qt::ApplicationActive); + return true; +} + +string OBSApp::GetVersionString(bool platform) const +{ + stringstream ver; + +#ifdef HAVE_OBSCONFIG_H + ver << obs_get_version_string(); +#else + ver << LIBOBS_API_MAJOR_VER << "." << LIBOBS_API_MINOR_VER << "." << LIBOBS_API_PATCH_VER; + +#endif + + if (platform) { + ver << " ("; +#ifdef _WIN32 + if (sizeof(void *) == 8) + ver << "64-bit, "; + else + ver << "32-bit, "; + + ver << "windows)"; +#elif __APPLE__ + ver << "mac)"; +#elif __OpenBSD__ + ver << "openbsd)"; +#elif __FreeBSD__ + ver << "freebsd)"; +#else /* assume linux for the time being */ + ver << "linux)"; +#endif + } + + return ver.str(); +} + +bool OBSApp::IsPortableMode() +{ + return portable_mode; +} + +bool OBSApp::IsUpdaterDisabled() +{ + return opt_disable_updater; +} + +bool OBSApp::IsMissingFilesCheckDisabled() +{ + return opt_disable_missing_files_check; +} + +#ifdef __APPLE__ +#define INPUT_AUDIO_SOURCE "coreaudio_input_capture" +#define OUTPUT_AUDIO_SOURCE "coreaudio_output_capture" +#elif _WIN32 +#define INPUT_AUDIO_SOURCE "wasapi_input_capture" +#define OUTPUT_AUDIO_SOURCE "wasapi_output_capture" +#else +#define INPUT_AUDIO_SOURCE "pulse_input_capture" +#define OUTPUT_AUDIO_SOURCE "pulse_output_capture" +#endif + +const char *OBSApp::InputAudioSource() const +{ + return INPUT_AUDIO_SOURCE; +} + +const char *OBSApp::OutputAudioSource() const +{ + return OUTPUT_AUDIO_SOURCE; +} + +const char *OBSApp::GetLastLog() const +{ + return lastLogFile.c_str(); +} + +const char *OBSApp::GetCurrentLog() const +{ + return currentLogFile.c_str(); +} + +const char *OBSApp::GetLastCrashLog() const +{ + return lastCrashLogFile.c_str(); +} + +bool OBSApp::TranslateString(const char *lookupVal, const char **out) const +{ + for (obs_frontend_translate_ui_cb cb : translatorHooks) { + if (cb(lookupVal, out)) + return true; + } + + return text_lookup_getstr(App()->GetTextLookup(), lookupVal, out); +} + +// Global handler to receive all QEvent::Show events so we can apply +// display affinity on any newly created windows and dialogs without +// caring where they are coming from (e.g. plugins). +bool OBSApp::notify(QObject *receiver, QEvent *e) +{ + QWidget *w; + QWindow *window; + int windowType; + + if (!receiver->isWidgetType()) + goto skip; + + if (e->type() != QEvent::Show) + goto skip; + + w = qobject_cast(receiver); + + if (!w->isWindow()) + goto skip; + + window = w->windowHandle(); + if (!window) + goto skip; + + windowType = window->flags() & Qt::WindowType::WindowType_Mask; + + if (windowType == Qt::WindowType::Dialog || windowType == Qt::WindowType::Window || + windowType == Qt::WindowType::Tool) { + OBSBasic *main = reinterpret_cast(GetMainWindow()); + if (main) + main->SetDisplayAffinity(window); + } + +skip: + return QApplication::notify(receiver, e); +} + +QString OBSTranslator::translate(const char *, const char *sourceText, const char *, int) const +{ + const char *out = nullptr; + QString str(sourceText); + str.replace(" ", ""); + if (!App()->TranslateString(QT_TO_UTF8(str), &out)) + return QString(sourceText); + + return QT_UTF8(out); +} + +static bool get_token(lexer *lex, string &str, base_token_type type) +{ + base_token token; + if (!lexer_getbasetoken(lex, &token, IGNORE_WHITESPACE)) + return false; + if (token.type != type) + return false; + + str.assign(token.text.array, token.text.len); + return true; +} + +static bool expect_token(lexer *lex, const char *str, base_token_type type) +{ + base_token token; + if (!lexer_getbasetoken(lex, &token, IGNORE_WHITESPACE)) + return false; + if (token.type != type) + return false; + + return strref_cmp(&token.text, str) == 0; +} + +static uint64_t convert_log_name(bool has_prefix, const char *name) +{ + BaseLexer lex; + string year, month, day, hour, minute, second; + + lexer_start(lex, name); + + if (has_prefix) { + string temp; + if (!get_token(lex, temp, BASETOKEN_ALPHA)) + return 0; + } + + if (!get_token(lex, year, BASETOKEN_DIGIT)) + return 0; + if (!expect_token(lex, "-", BASETOKEN_OTHER)) + return 0; + if (!get_token(lex, month, BASETOKEN_DIGIT)) + return 0; + if (!expect_token(lex, "-", BASETOKEN_OTHER)) + return 0; + if (!get_token(lex, day, BASETOKEN_DIGIT)) + return 0; + if (!get_token(lex, hour, BASETOKEN_DIGIT)) + return 0; + if (!expect_token(lex, "-", BASETOKEN_OTHER)) + return 0; + if (!get_token(lex, minute, BASETOKEN_DIGIT)) + return 0; + if (!expect_token(lex, "-", BASETOKEN_OTHER)) + return 0; + if (!get_token(lex, second, BASETOKEN_DIGIT)) + return 0; + + stringstream timestring; + timestring << year << month << day << hour << minute << second; + return std::stoull(timestring.str()); +} + +/* If upgrading from an older (non-XDG) build of OBS, move config files to XDG directory. */ +/* TODO: Remove after version 32.0. */ +#if defined(__FreeBSD__) +static void move_to_xdg(void) +{ + char old_path[512]; + char new_path[512]; + char *home = getenv("HOME"); + if (!home) + return; + + if (snprintf(old_path, sizeof(old_path), "%s/.obs-studio", home) <= 0) + return; + + /* make base xdg path if it doesn't already exist */ + if (GetAppConfigPath(new_path, sizeof(new_path), "") <= 0) + return; + if (os_mkdirs(new_path) == MKDIR_ERROR) + return; + + if (GetAppConfigPath(new_path, sizeof(new_path), "obs-studio") <= 0) + return; + + if (os_file_exists(old_path) && !os_file_exists(new_path)) { + rename(old_path, new_path); + } +} +#endif + +static void delete_oldest_file(bool has_prefix, const char *location) +{ + BPtr logDir(GetAppConfigPathPtr(location)); + string oldestLog; + uint64_t oldest_ts = (uint64_t)-1; + struct os_dirent *entry; + + unsigned int maxLogs = (unsigned int)config_get_uint(App()->GetAppConfig(), "General", "MaxLogs"); + + os_dir_t *dir = os_opendir(logDir); + if (dir) { + unsigned int count = 0; + + while ((entry = os_readdir(dir)) != NULL) { + if (entry->directory || *entry->d_name == '.') + continue; + + uint64_t ts = convert_log_name(has_prefix, entry->d_name); + + if (ts) { + if (ts < oldest_ts) { + oldestLog = entry->d_name; + oldest_ts = ts; + } + + count++; + } + } + + os_closedir(dir); + + if (count > maxLogs) { + stringstream delPath; + + delPath << logDir << "/" << oldestLog; + os_unlink(delPath.str().c_str()); + } + } +} + +static void get_last_log(bool has_prefix, const char *subdir_to_use, std::string &last) +{ + BPtr logDir(GetAppConfigPathPtr(subdir_to_use)); + struct os_dirent *entry; + os_dir_t *dir = os_opendir(logDir); + uint64_t highest_ts = 0; + + if (dir) { + while ((entry = os_readdir(dir)) != NULL) { + if (entry->directory || *entry->d_name == '.') + continue; + + uint64_t ts = convert_log_name(has_prefix, entry->d_name); + + if (ts > highest_ts) { + last = entry->d_name; + highest_ts = ts; + } + } + + os_closedir(dir); + } +} + +string GenerateTimeDateFilename(const char *extension, bool noSpace) +{ + time_t now = time(0); + char file[256] = {}; + struct tm *cur_time; + + cur_time = localtime(&now); + snprintf(file, sizeof(file), "%d-%02d-%02d%c%02d-%02d-%02d.%s", cur_time->tm_year + 1900, cur_time->tm_mon + 1, + cur_time->tm_mday, noSpace ? '_' : ' ', cur_time->tm_hour, cur_time->tm_min, cur_time->tm_sec, + extension); + + return string(file); +} + +string GenerateSpecifiedFilename(const char *extension, bool noSpace, const char *format) +{ + BPtr filename = os_generate_formatted_filename(extension, !noSpace, format); + return string(filename); +} + +static void FindBestFilename(string &strPath, bool noSpace) +{ + int num = 2; + + if (!os_file_exists(strPath.c_str())) + return; + + const char *ext = strrchr(strPath.c_str(), '.'); + if (!ext) + return; + + int extStart = int(ext - strPath.c_str()); + for (;;) { + string testPath = strPath; + string numStr; + + numStr = noSpace ? "_" : " ("; + numStr += to_string(num++); + if (!noSpace) + numStr += ")"; + + testPath.insert(extStart, numStr); + + if (!os_file_exists(testPath.c_str())) { + strPath = testPath; + break; + } + } +} + +static void ensure_directory_exists(string &path) +{ + replace(path.begin(), path.end(), '\\', '/'); + + size_t last = path.rfind('/'); + if (last == string::npos) + return; + + string directory = path.substr(0, last); + os_mkdirs(directory.c_str()); +} + +static void remove_reserved_file_characters(string &s) +{ + replace(s.begin(), s.end(), '\\', '/'); + replace(s.begin(), s.end(), '*', '_'); + replace(s.begin(), s.end(), '?', '_'); + replace(s.begin(), s.end(), '"', '_'); + replace(s.begin(), s.end(), '|', '_'); + replace(s.begin(), s.end(), ':', '_'); + replace(s.begin(), s.end(), '>', '_'); + replace(s.begin(), s.end(), '<', '_'); +} + +string GetFormatString(const char *format, const char *prefix, const char *suffix) +{ + string f; + + f = format; + + if (prefix && *prefix) { + string str_prefix = prefix; + + if (str_prefix.back() != ' ') + str_prefix += " "; + + size_t insert_pos = 0; + size_t tmp; + + tmp = f.find_last_of('/'); + if (tmp != string::npos && tmp > insert_pos) + insert_pos = tmp + 1; + + tmp = f.find_last_of('\\'); + if (tmp != string::npos && tmp > insert_pos) + insert_pos = tmp + 1; + + f.insert(insert_pos, str_prefix); + } + + if (suffix && *suffix) { + if (*suffix != ' ') + f += " "; + f += suffix; + } + + remove_reserved_file_characters(f); + + return f; +} + +string GetFormatExt(const char *container) +{ + string ext = container; + if (ext == "fragmented_mp4") + ext = "mp4"; + if (ext == "hybrid_mp4") + ext = "mp4"; + else if (ext == "fragmented_mov") + ext = "mov"; + else if (ext == "hls") + ext = "m3u8"; + else if (ext == "mpegts") + ext = "ts"; + + return ext; +} + +string GetOutputFilename(const char *path, const char *container, bool noSpace, bool overwrite, const char *format) +{ + OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); + + os_dir_t *dir = path && path[0] ? os_opendir(path) : nullptr; + + if (!dir) { + if (main->isVisible()) + OBSMessageBox::warning(main, QTStr("Output.BadPath.Title"), QTStr("Output.BadPath.Text")); + else + main->SysTrayNotify(QTStr("Output.BadPath.Text"), QSystemTrayIcon::Warning); + return ""; + } + + os_closedir(dir); + + string strPath; + strPath += path; + + char lastChar = strPath.back(); + if (lastChar != '/' && lastChar != '\\') + strPath += "/"; + + string ext = GetFormatExt(container); + strPath += GenerateSpecifiedFilename(ext.c_str(), noSpace, format); + ensure_directory_exists(strPath); + if (!overwrite) + FindBestFilename(strPath, noSpace); + + return strPath; +} + +vector> GetLocaleNames() +{ + string path; + if (!GetDataFilePath("locale.ini", path)) + throw "Could not find locale.ini path"; + + ConfigFile ini; + if (ini.Open(path.c_str(), CONFIG_OPEN_EXISTING) != 0) + throw "Could not open locale.ini"; + + size_t sections = config_num_sections(ini); + + vector> names; + names.reserve(sections); + for (size_t i = 0; i < sections; i++) { + const char *tag = config_get_section(ini, i); + const char *name = config_get_string(ini, tag, "Name"); + names.emplace_back(tag, name); + } + + return names; +} + +static void create_log_file(fstream &logFile) +{ + stringstream dst; + + get_last_log(false, "obs-studio/logs", lastLogFile); +#ifdef _WIN32 + get_last_log(true, "obs-studio/crashes", lastCrashLogFile); +#endif + + currentLogFile = GenerateTimeDateFilename("txt"); + dst << "obs-studio/logs/" << currentLogFile.c_str(); + + BPtr path(GetAppConfigPathPtr(dst.str().c_str())); + +#ifdef _WIN32 + BPtr wpath; + os_utf8_to_wcs_ptr(path, 0, &wpath); + logFile.open(wpath, ios_base::in | ios_base::out | ios_base::trunc); +#else + logFile.open(path, ios_base::in | ios_base::out | ios_base::trunc); +#endif + + if (logFile.is_open()) { + delete_oldest_file(false, "obs-studio/logs"); + base_set_log_handler(do_log, &logFile); + } else { + blog(LOG_ERROR, "Failed to open log file"); + } +} + +static auto ProfilerNameStoreRelease = [](profiler_name_store_t *store) { + profiler_name_store_free(store); +}; + +using ProfilerNameStore = std::unique_ptr; + +ProfilerNameStore CreateNameStore() +{ + return ProfilerNameStore{profiler_name_store_create(), ProfilerNameStoreRelease}; +} + +static auto SnapshotRelease = [](profiler_snapshot_t *snap) { + profile_snapshot_free(snap); +}; + +using ProfilerSnapshot = std::unique_ptr; + +ProfilerSnapshot GetSnapshot() +{ + return ProfilerSnapshot{profile_snapshot_create(), SnapshotRelease}; +} + +static void SaveProfilerData(const ProfilerSnapshot &snap) +{ + if (currentLogFile.empty()) + return; + + auto pos = currentLogFile.rfind('.'); + if (pos == currentLogFile.npos) + return; + +#define LITERAL_SIZE(x) x, (sizeof(x) - 1) + ostringstream dst; + dst.write(LITERAL_SIZE("obs-studio/profiler_data/")); + dst.write(currentLogFile.c_str(), pos); + dst.write(LITERAL_SIZE(".csv.gz")); +#undef LITERAL_SIZE + + BPtr path = GetAppConfigPathPtr(dst.str().c_str()); + if (!profiler_snapshot_dump_csv_gz(snap.get(), path)) + blog(LOG_WARNING, "Could not save profiler data to '%s'", static_cast(path)); +} + +static auto ProfilerFree = [](void *) { + profiler_stop(); + + auto snap = GetSnapshot(); + + profiler_print(snap.get()); + profiler_print_time_between_calls(snap.get()); + + SaveProfilerData(snap); + + profiler_free(); +}; + +QAccessibleInterface *accessibleFactory(const QString &classname, QObject *object) +{ + if (classname == QLatin1String("VolumeSlider") && object && object->isWidgetType()) + return new VolumeAccessibleInterface(static_cast(object)); + + return nullptr; +} + +static const char *run_program_init = "run_program_init"; +static int run_program(fstream &logFile, int argc, char *argv[]) +{ + int ret = -1; + + auto profilerNameStore = CreateNameStore(); + + std::unique_ptr prof_release(static_cast(&ProfilerFree), ProfilerFree); + + profiler_start(); + profile_register_root(run_program_init, 0); + + ScopeProfiler prof{run_program_init}; + +#ifdef _WIN32 + QGuiApplication::setHighDpiScaleFactorRoundingPolicy(Qt::HighDpiScaleFactorRoundingPolicy::PassThrough); +#endif + + QCoreApplication::addLibraryPath("."); + +#if __APPLE__ + InstallNSApplicationSubclass(); + InstallNSThreadLocks(); + + if (!isInBundle()) { + blog(LOG_ERROR, + "OBS cannot be run as a standalone binary on macOS. Run the Application bundle instead."); + return ret; + } +#endif + +#if !defined(_WIN32) && !defined(__APPLE__) + /* NOTE: Users blindly set this, but this theme is incompatble with Qt6 and + * crashes loading saved geometry. Just turn off this theme and let users complain OBS + * looks ugly instead of crashing. */ + const char *platform_theme = getenv("QT_QPA_PLATFORMTHEME"); + if (platform_theme && strcmp(platform_theme, "qt5ct") == 0) + unsetenv("QT_QPA_PLATFORMTHEME"); +#endif + + /* NOTE: This disables an optimisation in Qt that attempts to determine if + * any "siblings" intersect with a widget when determining the approximate + * visible/unobscured area. However, by Qt's own admission this is slow + * and in the case of OBS it significantly slows down lists with many + * elements (e.g. Hotkeys) and it is actually faster to disable it. */ + qputenv("QT_NO_SUBTRACTOPAQUESIBLINGS", "1"); + + OBSApp program(argc, argv, profilerNameStore.get()); + try { + QAccessible::installFactory(accessibleFactory); + QFontDatabase::addApplicationFont(":/fonts/OpenSans-Regular.ttf"); + QFontDatabase::addApplicationFont(":/fonts/OpenSans-Bold.ttf"); + QFontDatabase::addApplicationFont(":/fonts/OpenSans-Italic.ttf"); + + bool created_log = false; + + program.AppInit(); + delete_oldest_file(false, "obs-studio/profiler_data"); + + OBSTranslator translator; + program.installTranslator(&translator); + + /* --------------------------------------- */ + /* check and warn if already running */ + + bool cancel_launch = false; + bool already_running = false; + +#ifdef _WIN32 + RunOnceMutex rom = +#endif + CheckIfAlreadyRunning(already_running); + + if (!already_running) { + goto run; + } + + if (!multi) { + QMessageBox mb(QMessageBox::Question, QTStr("AlreadyRunning.Title"), + QTStr("AlreadyRunning.Text")); + mb.addButton(QTStr("AlreadyRunning.LaunchAnyway"), QMessageBox::YesRole); + QPushButton *cancelButton = mb.addButton(QTStr("Cancel"), QMessageBox::NoRole); + mb.setDefaultButton(cancelButton); + + mb.exec(); + cancel_launch = mb.clickedButton() == cancelButton; + } + + if (cancel_launch) + return 0; + + if (!created_log) { + create_log_file(logFile); + created_log = true; + } + + if (multi) { + blog(LOG_INFO, "User enabled --multi flag and is now " + "running multiple instances of OBS."); + } else { + blog(LOG_WARNING, "================================"); + blog(LOG_WARNING, "Warning: OBS is already running!"); + blog(LOG_WARNING, "================================"); + blog(LOG_WARNING, "User is now running multiple " + "instances of OBS!"); + /* Clear unclean_shutdown flag as multiple instances + * running from the same config will lead to a + * false-positive detection.*/ + unclean_shutdown = false; + } + + /* --------------------------------------- */ + run: + +#if !defined(_WIN32) && !defined(__APPLE__) && !defined(__FreeBSD__) + // Mounted by termina during chromeOS linux container startup + // https://chromium.googlesource.com/chromiumos/overlays/board-overlays/+/master/project-termina/chromeos-base/termina-lxd-scripts/files/lxd_setup.sh + os_dir_t *crosDir = os_opendir("/opt/google/cros-containers"); + if (crosDir) { + QMessageBox::StandardButtons buttons(QMessageBox::Ok); + QMessageBox mb(QMessageBox::Critical, QTStr("ChromeOS.Title"), QTStr("ChromeOS.Text"), buttons, + nullptr); + + mb.exec(); + return 0; + } +#endif + + if (!created_log) + create_log_file(logFile); + + if (unclean_shutdown) { + blog(LOG_WARNING, "[Safe Mode] Unclean shutdown detected!"); + } + + if (unclean_shutdown && !safe_mode) { + QMessageBox mb(QMessageBox::Warning, QTStr("AutoSafeMode.Title"), QTStr("AutoSafeMode.Text")); + QPushButton *launchSafeButton = + mb.addButton(QTStr("AutoSafeMode.LaunchSafe"), QMessageBox::AcceptRole); + QPushButton *launchNormalButton = + mb.addButton(QTStr("AutoSafeMode.LaunchNormal"), QMessageBox::RejectRole); + mb.setDefaultButton(launchNormalButton); + mb.exec(); + + safe_mode = mb.clickedButton() == launchSafeButton; + if (safe_mode) { + blog(LOG_INFO, "[Safe Mode] User has launched in Safe Mode."); + } else { + blog(LOG_WARNING, "[Safe Mode] User elected to launch normally."); + } + } + + qInstallMessageHandler([](QtMsgType type, const QMessageLogContext &, const QString &message) { + switch (type) { +#ifdef _DEBUG + case QtDebugMsg: + blog(LOG_DEBUG, "%s", QT_TO_UTF8(message)); + break; + case QtInfoMsg: + blog(LOG_INFO, "%s", QT_TO_UTF8(message)); + break; +#else + case QtDebugMsg: + case QtInfoMsg: + break; +#endif + case QtWarningMsg: + blog(LOG_WARNING, "%s", QT_TO_UTF8(message)); + break; + case QtCriticalMsg: + case QtFatalMsg: + blog(LOG_ERROR, "%s", QT_TO_UTF8(message)); + break; + } + }); + +#ifdef __APPLE__ + MacPermissionStatus audio_permission = CheckPermission(kAudioDeviceAccess); + MacPermissionStatus video_permission = CheckPermission(kVideoDeviceAccess); + MacPermissionStatus accessibility_permission = CheckPermission(kAccessibility); + MacPermissionStatus screen_permission = CheckPermission(kScreenCapture); + + int permissionsDialogLastShown = + config_get_int(App()->GetAppConfig(), "General", "MacOSPermissionsDialogLastShown"); + if (permissionsDialogLastShown < MACOS_PERMISSIONS_DIALOG_VERSION) { + OBSPermissions check(nullptr, screen_permission, video_permission, audio_permission, + accessibility_permission); + check.exec(); + } +#endif + +#ifdef _WIN32 + if (IsRunningOnWine()) { + QMessageBox mb(QMessageBox::Question, QTStr("Wine.Title"), QTStr("Wine.Text")); + mb.setTextFormat(Qt::RichText); + mb.addButton(QTStr("AlreadyRunning.LaunchAnyway"), QMessageBox::AcceptRole); + QPushButton *closeButton = mb.addButton(QMessageBox::Close); + mb.setDefaultButton(closeButton); + + mb.exec(); + if (mb.clickedButton() == closeButton) + return 0; + } +#endif + + if (argc > 1) { + stringstream stor; + stor << argv[1]; + for (int i = 2; i < argc; ++i) { + stor << " " << argv[i]; + } + blog(LOG_INFO, "Command Line Arguments: %s", stor.str().c_str()); + } + + if (!program.OBSInit()) + return 0; + + prof.Stop(); + + ret = program.exec(); + + } catch (const char *error) { + blog(LOG_ERROR, "%s", error); + OBSErrorBox(nullptr, "%s", error); + } + + if (restart || restart_safe) { + arguments = qApp->arguments(); + + if (restart_safe) { + arguments.append("--safe-mode"); + } else { + arguments.removeAll("--safe-mode"); + } + } + + return ret; +} + +#define MAX_CRASH_REPORT_SIZE (150 * 1024) + +#ifdef _WIN32 + +#define CRASH_MESSAGE \ + "Woops, OBS has crashed!\n\nWould you like to copy the crash log " \ + "to the clipboard? The crash log will still be saved to:\n\n%s" + +static void main_crash_handler(const char *format, va_list args, void * /* param */) +{ + char *text = new char[MAX_CRASH_REPORT_SIZE]; + + vsnprintf(text, MAX_CRASH_REPORT_SIZE, format, args); + text[MAX_CRASH_REPORT_SIZE - 1] = 0; + + string crashFilePath = "obs-studio/crashes"; + + delete_oldest_file(true, crashFilePath.c_str()); + + string name = crashFilePath + "/"; + name += "Crash " + GenerateTimeDateFilename("txt"); + + BPtr path(GetAppConfigPathPtr(name.c_str())); + + fstream file; + +#ifdef _WIN32 + BPtr wpath; + os_utf8_to_wcs_ptr(path, 0, &wpath); + file.open(wpath, ios_base::in | ios_base::out | ios_base::trunc | ios_base::binary); +#else + file.open(path, ios_base::in | ios_base::out | ios_base::trunc | ios_base::binary); +#endif + file << text; + file.close(); + + string pathString(path.Get()); + +#ifdef _WIN32 + std::replace(pathString.begin(), pathString.end(), '/', '\\'); +#endif + + string absolutePath = canonical(filesystem::path(pathString)).u8string(); + + size_t size = snprintf(nullptr, 0, CRASH_MESSAGE, absolutePath.c_str()); + + unique_ptr message_buffer(new char[size + 1]); + + snprintf(message_buffer.get(), size + 1, CRASH_MESSAGE, absolutePath.c_str()); + + string finalMessage = string(message_buffer.get(), message_buffer.get() + size); + + int ret = MessageBoxA(NULL, finalMessage.c_str(), "OBS has crashed!", MB_YESNO | MB_ICONERROR | MB_TASKMODAL); + + if (ret == IDYES) { + size_t len = strlen(text); + + HGLOBAL mem = GlobalAlloc(GMEM_MOVEABLE, len); + memcpy(GlobalLock(mem), text, len); + GlobalUnlock(mem); + + OpenClipboard(0); + EmptyClipboard(); + SetClipboardData(CF_TEXT, mem); + CloseClipboard(); + } + + exit(-1); +} + +static void load_debug_privilege(void) +{ + const DWORD flags = TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY; + TOKEN_PRIVILEGES tp; + HANDLE token; + LUID val; + + if (!OpenProcessToken(GetCurrentProcess(), flags, &token)) { + return; + } + + if (!!LookupPrivilegeValue(NULL, SE_DEBUG_NAME, &val)) { + tp.PrivilegeCount = 1; + tp.Privileges[0].Luid = val; + tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED; + + AdjustTokenPrivileges(token, false, &tp, sizeof(tp), NULL, NULL); + } + + if (!!LookupPrivilegeValue(NULL, SE_INC_BASE_PRIORITY_NAME, &val)) { + tp.PrivilegeCount = 1; + tp.Privileges[0].Luid = val; + tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED; + + if (!AdjustTokenPrivileges(token, false, &tp, sizeof(tp), NULL, NULL)) { + blog(LOG_INFO, "Could not set privilege to " + "increase GPU priority"); + } + } + + CloseHandle(token); +} +#endif + +#ifdef __APPLE__ +#define BASE_PATH ".." +#else +#define BASE_PATH "../.." +#endif + +#define CONFIG_PATH BASE_PATH "/config" + +#if defined(ENABLE_PORTABLE_CONFIG) || defined(_WIN32) +#define ALLOW_PORTABLE_MODE 1 +#else +#define ALLOW_PORTABLE_MODE 0 +#endif + +int GetAppConfigPath(char *path, size_t size, const char *name) +{ +#if ALLOW_PORTABLE_MODE + if (portable_mode) { + if (name && *name) { + return snprintf(path, size, CONFIG_PATH "/%s", name); + } else { + return snprintf(path, size, CONFIG_PATH); + } + } else { + return os_get_config_path(path, size, name); + } +#else + return os_get_config_path(path, size, name); +#endif +} + +char *GetAppConfigPathPtr(const char *name) +{ +#if ALLOW_PORTABLE_MODE + if (portable_mode) { + char path[512]; + + if (snprintf(path, sizeof(path), CONFIG_PATH "/%s", name) > 0) { + return bstrdup(path); + } else { + return NULL; + } + } else { + return os_get_config_path_ptr(name); + } +#else + return os_get_config_path_ptr(name); +#endif +} + +int GetProgramDataPath(char *path, size_t size, const char *name) +{ + return os_get_program_data_path(path, size, name); +} + +char *GetProgramDataPathPtr(const char *name) +{ + return os_get_program_data_path_ptr(name); +} + +bool GetFileSafeName(const char *name, std::string &file) +{ + size_t base_len = strlen(name); + size_t len = os_utf8_to_wcs(name, base_len, nullptr, 0); + std::wstring wfile; + + if (!len) + return false; + + wfile.resize(len); + os_utf8_to_wcs(name, base_len, &wfile[0], len + 1); + + for (size_t i = wfile.size(); i > 0; i--) { + size_t im1 = i - 1; + + if (iswspace(wfile[im1])) { + wfile[im1] = '_'; + } else if (wfile[im1] != '_' && !iswalnum(wfile[im1])) { + wfile.erase(im1, 1); + } + } + + if (wfile.size() == 0) + wfile = L"characters_only"; + + len = os_wcs_to_utf8(wfile.c_str(), wfile.size(), nullptr, 0); + if (!len) + return false; + + file.resize(len); + os_wcs_to_utf8(wfile.c_str(), wfile.size(), &file[0], len + 1); + return true; +} + +bool GetClosestUnusedFileName(std::string &path, const char *extension) +{ + size_t len = path.size(); + if (extension) { + path += "."; + path += extension; + } + + if (!os_file_exists(path.c_str())) + return true; + + int index = 1; + + do { + path.resize(len); + path += std::to_string(++index); + if (extension) { + path += "."; + path += extension; + } + } while (os_file_exists(path.c_str())); + + return true; +} + +bool WindowPositionValid(QRect rect) +{ + for (QScreen *screen : QGuiApplication::screens()) { + if (screen->availableGeometry().intersects(rect)) + return true; + } + return false; +} + +static inline bool arg_is(const char *arg, const char *long_form, const char *short_form) +{ + return (long_form && strcmp(arg, long_form) == 0) || (short_form && strcmp(arg, short_form) == 0); +} + +static void check_safe_mode_sentinel(void) +{ +#ifndef NDEBUG + /* Safe Mode detection is disabled in Debug builds to keep developers + * somewhat sane. */ + return; +#else + if (disable_shutdown_check) + return; + + BPtr sentinelPath = GetAppConfigPathPtr("obs-studio/safe_mode"); + if (os_file_exists(sentinelPath)) { + unclean_shutdown = true; + return; + } + + os_quick_write_utf8_file(sentinelPath, nullptr, 0, false); +#endif +} + +static void delete_safe_mode_sentinel(void) +{ +#ifndef NDEBUG + return; +#else + BPtr sentinelPath = GetAppConfigPathPtr("obs-studio/safe_mode"); + os_unlink(sentinelPath); +#endif +} + +#ifndef _WIN32 +void OBSApp::SigIntSignalHandler(int s) +{ + /* Handles SIGINT and writes to a socket. Qt will read + * from the socket in the main thread event loop and trigger + * a call to the ProcessSigInt slot, where we can safely run + * shutdown code without signal safety issues. */ + UNUSED_PARAMETER(s); + + char a = 1; + send(sigintFd[0], &a, sizeof(a), 0); +} +#endif + +void OBSApp::ProcessSigInt(void) +{ + /* This looks weird, but we can't ifdef a Qt slot function so + * the SIGINT handler simply does nothing on Windows. */ +#ifndef _WIN32 + char tmp; + recv(sigintFd[1], &tmp, sizeof(tmp), 0); + + OBSBasic *main = reinterpret_cast(GetMainWindow()); + if (main) + main->close(); +#endif +} + +#ifdef _WIN32 +void OBSApp::commitData(QSessionManager &manager) +{ + if (auto main = App()->GetMainWindow()) { + QMetaObject::invokeMethod(main, "close", Qt::QueuedConnection); + manager.cancel(); + } +} +#endif + +#ifdef _WIN32 +static constexpr char vcRunErrorTitle[] = "Outdated Visual C++ Runtime"; +static constexpr char vcRunErrorMsg[] = "OBS Studio requires a newer version of the Microsoft Visual C++ " + "Redistributables.\n\nYou will now be directed to the download page."; +static constexpr char vcRunInstallerUrl[] = "https://obsproject.com/visual-studio-2022-runtimes"; + +static bool vc_runtime_outdated() +{ + win_version_info ver; + if (!get_dll_ver(L"msvcp140.dll", &ver)) + return true; + /* Major is always 14 (hence 140.dll), so we only care about minor. */ + if (ver.minor >= 40) + return false; + + int choice = MessageBoxA(NULL, vcRunErrorMsg, vcRunErrorTitle, MB_OKCANCEL | MB_ICONERROR | MB_TASKMODAL); + if (choice == IDOK) { + /* Open the URL in the default browser. */ + ShellExecuteA(NULL, "open", vcRunInstallerUrl, NULL, NULL, SW_SHOWNORMAL); + } + + return true; +} +#endif + +int main(int argc, char *argv[]) +{ +#ifndef _WIN32 + signal(SIGPIPE, SIG_IGN); + + struct sigaction sig_handler; + + sig_handler.sa_handler = OBSApp::SigIntSignalHandler; + sigemptyset(&sig_handler.sa_mask); + sig_handler.sa_flags = 0; + + sigaction(SIGINT, &sig_handler, NULL); + + /* Block SIGPIPE in all threads, this can happen if a thread calls write on + a closed pipe. */ + sigset_t sigpipe_mask; + sigemptyset(&sigpipe_mask); + sigaddset(&sigpipe_mask, SIGPIPE); + sigset_t saved_mask; + if (pthread_sigmask(SIG_BLOCK, &sigpipe_mask, &saved_mask) == -1) { + perror("pthread_sigmask"); + exit(1); + } +#endif + +#ifdef _WIN32 + // Abort as early as possible if MSVC runtime is outdated + if (vc_runtime_outdated()) + return 1; + // Try to keep this as early as possible + install_dll_blocklist_hook(); + + obs_init_win32_crash_handler(); + SetErrorMode(SEM_FAILCRITICALERRORS); + load_debug_privilege(); + base_set_crash_handler(main_crash_handler, nullptr); + + const HMODULE hRtwq = LoadLibrary(L"RTWorkQ.dll"); + if (hRtwq) { + typedef HRESULT(STDAPICALLTYPE * PFN_RtwqStartup)(); + PFN_RtwqStartup func = (PFN_RtwqStartup)GetProcAddress(hRtwq, "RtwqStartup"); + func(); + } +#endif + + base_get_log_handler(&def_log_handler, nullptr); + +#if defined(__FreeBSD__) + move_to_xdg(); +#endif + + obs_set_cmdline_args(argc, argv); + + for (int i = 1; i < argc; i++) { + if (arg_is(argv[i], "--multi", "-m")) { + multi = true; + disable_shutdown_check = true; + +#if ALLOW_PORTABLE_MODE + } else if (arg_is(argv[i], "--portable", "-p")) { + portable_mode = true; + +#endif + } else if (arg_is(argv[i], "--verbose", nullptr)) { + log_verbose = true; + + } else if (arg_is(argv[i], "--safe-mode", nullptr)) { + safe_mode = true; + + } else if (arg_is(argv[i], "--only-bundled-plugins", nullptr)) { + disable_3p_plugins = true; + + } else if (arg_is(argv[i], "--disable-shutdown-check", nullptr)) { + /* This exists mostly to bypass the dialog during development. */ + disable_shutdown_check = true; + + } else if (arg_is(argv[i], "--always-on-top", nullptr)) { + opt_always_on_top = true; + + } else if (arg_is(argv[i], "--unfiltered_log", nullptr)) { + unfiltered_log = true; + + } else if (arg_is(argv[i], "--startstreaming", nullptr)) { + opt_start_streaming = true; + + } else if (arg_is(argv[i], "--startrecording", nullptr)) { + opt_start_recording = true; + + } else if (arg_is(argv[i], "--startreplaybuffer", nullptr)) { + opt_start_replaybuffer = true; + + } else if (arg_is(argv[i], "--startvirtualcam", nullptr)) { + opt_start_virtualcam = true; + + } else if (arg_is(argv[i], "--collection", nullptr)) { + if (++i < argc) + opt_starting_collection = argv[i]; + + } else if (arg_is(argv[i], "--profile", nullptr)) { + if (++i < argc) + opt_starting_profile = argv[i]; + + } else if (arg_is(argv[i], "--scene", nullptr)) { + if (++i < argc) + opt_starting_scene = argv[i]; + + } else if (arg_is(argv[i], "--minimize-to-tray", nullptr)) { + opt_minimize_tray = true; + + } else if (arg_is(argv[i], "--studio-mode", nullptr)) { + opt_studio_mode = true; + + } else if (arg_is(argv[i], "--allow-opengl", nullptr)) { + opt_allow_opengl = true; + + } else if (arg_is(argv[i], "--disable-updater", nullptr)) { + opt_disable_updater = true; + + } else if (arg_is(argv[i], "--disable-missing-files-check", nullptr)) { + opt_disable_missing_files_check = true; + + } else if (arg_is(argv[i], "--steam", nullptr)) { + steam = true; + + } else if (arg_is(argv[i], "--help", "-h")) { + std::string help = + "--help, -h: Get list of available commands.\n\n" + "--startstreaming: Automatically start streaming.\n" + "--startrecording: Automatically start recording.\n" + "--startreplaybuffer: Start replay buffer.\n" + "--startvirtualcam: Start virtual camera (if available).\n\n" + "--collection : Use specific scene collection." + "\n" + "--profile : Use specific profile.\n" + "--scene : Start with specific scene.\n\n" + "--studio-mode: Enable studio mode.\n" + "--minimize-to-tray: Minimize to system tray.\n" +#if ALLOW_PORTABLE_MODE + "--portable, -p: Use portable mode.\n" +#endif + "--multi, -m: Don't warn when launching multiple instances.\n\n" + "--safe-mode: Run in Safe Mode (disables third-party plugins, scripting, and WebSockets).\n" + "--only-bundled-plugins: Only load included (first-party) plugins\n" + "--disable-shutdown-check: Disable unclean shutdown detection.\n" + "--verbose: Make log more verbose.\n" + "--always-on-top: Start in 'always on top' mode.\n\n" + "--unfiltered_log: Make log unfiltered.\n\n" + "--disable-updater: Disable built-in updater (Windows/Mac only)\n\n" + "--disable-missing-files-check: Disable the missing files dialog which can appear on startup.\n\n"; + +#ifdef _WIN32 + MessageBoxA(NULL, help.c_str(), "Help", MB_OK | MB_ICONASTERISK); +#else + std::cout << help << "--version, -V: Get current version.\n"; +#endif + exit(0); + + } else if (arg_is(argv[i], "--version", "-V")) { + std::cout << "OBS Studio - " << App()->GetVersionString(false) << "\n"; + exit(0); + } + } + +#if ALLOW_PORTABLE_MODE + if (!portable_mode) { + portable_mode = os_file_exists(BASE_PATH "/portable_mode") || + os_file_exists(BASE_PATH "/obs_portable_mode") || + os_file_exists(BASE_PATH "/portable_mode.txt") || + os_file_exists(BASE_PATH "/obs_portable_mode.txt"); + } + + if (!opt_disable_updater) { + opt_disable_updater = os_file_exists(BASE_PATH "/disable_updater") || + os_file_exists(BASE_PATH "/disable_updater.txt"); + } + + if (!opt_disable_missing_files_check) { + opt_disable_missing_files_check = os_file_exists(BASE_PATH "/disable_missing_files_check") || + os_file_exists(BASE_PATH "/disable_missing_files_check.txt"); + } +#endif + + check_safe_mode_sentinel(); + + fstream logFile; + + curl_global_init(CURL_GLOBAL_ALL); + int ret = run_program(logFile, argc, argv); + +#ifdef _WIN32 + if (hRtwq) { + typedef HRESULT(STDAPICALLTYPE * PFN_RtwqShutdown)(); + PFN_RtwqShutdown func = (PFN_RtwqShutdown)GetProcAddress(hRtwq, "RtwqShutdown"); + func(); + FreeLibrary(hRtwq); + } + + log_blocked_dlls(); +#endif + + delete_safe_mode_sentinel(); + blog(LOG_INFO, "Number of memory leaks: %ld", bnum_allocs()); + base_set_log_handler(nullptr, nullptr); + + if (restart || restart_safe) { + auto executable = arguments.takeFirst(); + QProcess::startDetached(executable, arguments); + } + + return ret; +} diff --git a/frontend/utility/BaseLexer.hpp b/frontend/utility/BaseLexer.hpp new file mode 100644 index 000000000..dea5e530b --- /dev/null +++ b/frontend/utility/BaseLexer.hpp @@ -0,0 +1,280 @@ +/****************************************************************************** + Copyright (C) 2023 by Lain Bailey + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ + +#pragma once + +#include +#include +#include +#include + +#ifndef _WIN32 +#include +#else +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "window-main.hpp" +#include "obs-app-theming.hpp" + +std::string CurrentTimeString(); +std::string CurrentDateTimeString(); +std::string GenerateTimeDateFilename(const char *extension, bool noSpace = false); +std::string GenerateSpecifiedFilename(const char *extension, bool noSpace, const char *format); +std::string GetFormatString(const char *format, const char *prefix, const char *suffix); +std::string GetFormatExt(const char *container); +std::string GetOutputFilename(const char *path, const char *container, bool noSpace, bool overwrite, + const char *format); +QObject *CreateShortcutFilter(); + +struct BaseLexer { + lexer lex; + +public: + inline BaseLexer() { lexer_init(&lex); } + inline ~BaseLexer() { lexer_free(&lex); } + operator lexer *() { return &lex; } +}; + +class OBSTranslator : public QTranslator { + Q_OBJECT + +public: + virtual bool isEmpty() const override { return false; } + + virtual QString translate(const char *context, const char *sourceText, const char *disambiguation, + int n) const override; +}; + +typedef std::function VoidFunc; + +struct UpdateBranch { + QString name; + QString display_name; + QString description; + bool is_enabled; + bool is_visible; +}; + +class OBSApp : public QApplication { + Q_OBJECT + +private: + std::string locale; + + ConfigFile appConfig; + ConfigFile userConfig; + TextLookup textLookup; + QPointer mainWindow; + profiler_name_store_t *profilerNameStore = nullptr; + std::vector updateBranches; + bool branches_loaded = false; + + bool libobs_initialized = false; + + os_inhibit_t *sleepInhibitor = nullptr; + int sleepInhibitRefs = 0; + + bool enableHotkeysInFocus = true; + bool enableHotkeysOutOfFocus = true; + + std::deque translatorHooks; + + bool UpdatePre22MultiviewLayout(const char *layout); + + bool InitGlobalConfig(); + bool InitGlobalConfigDefaults(); + bool InitGlobalLocationDefaults(); + + bool MigrateGlobalSettings(); + void MigrateLegacySettings(uint32_t lastVersion); + + bool InitUserConfig(std::filesystem::path &userConfigLocation, uint32_t lastVersion); + void InitUserConfigDefaults(); + + bool InitLocale(); + bool InitTheme(); + + inline void ResetHotkeyState(bool inFocus); + + QPalette defaultPalette; + OBSTheme *currentTheme = nullptr; + QHash themes; + QPointer themeWatcher; + + void FindThemes(); + + bool notify(QObject *receiver, QEvent *e) override; + +#ifndef _WIN32 + static int sigintFd[2]; + QSocketNotifier *snInt = nullptr; +#else +private slots: + void commitData(QSessionManager &manager); +#endif + +private slots: + void themeFileChanged(const QString &); + +public: + OBSApp(int &argc, char **argv, profiler_name_store_t *store); + ~OBSApp(); + + void AppInit(); + bool OBSInit(); + + void UpdateHotkeyFocusSetting(bool reset = true); + void DisableHotkeys(); + + inline bool HotkeysEnabledInFocus() const { return enableHotkeysInFocus; } + + inline QMainWindow *GetMainWindow() const { return mainWindow.data(); } + + inline config_t *GetAppConfig() const { return appConfig; } + inline config_t *GetUserConfig() const { return userConfig; } + std::filesystem::path userConfigLocation; + std::filesystem::path userScenesLocation; + std::filesystem::path userProfilesLocation; + + inline const char *GetLocale() const { return locale.c_str(); } + + OBSTheme *GetTheme() const { return currentTheme; } + QList GetThemes() const { return themes.values(); } + OBSTheme *GetTheme(const QString &name); + bool SetTheme(const QString &name); + bool IsThemeDark() const { return currentTheme ? currentTheme->isDark : false; } + + void SetBranchData(const std::string &data); + std::vector GetBranches(); + + inline lookup_t *GetTextLookup() const { return textLookup; } + + inline const char *GetString(const char *lookupVal) const { return textLookup.GetString(lookupVal); } + + bool TranslateString(const char *lookupVal, const char **out) const; + + profiler_name_store_t *GetProfilerNameStore() const { return profilerNameStore; } + + const char *GetLastLog() const; + const char *GetCurrentLog() const; + + const char *GetLastCrashLog() const; + + std::string GetVersionString(bool platform = true) const; + bool IsPortableMode(); + bool IsUpdaterDisabled(); + bool IsMissingFilesCheckDisabled(); + + const char *InputAudioSource() const; + const char *OutputAudioSource() const; + + const char *GetRenderModule() const; + + inline void IncrementSleepInhibition() + { + if (!sleepInhibitor) + return; + if (sleepInhibitRefs++ == 0) + os_inhibit_sleep_set_active(sleepInhibitor, true); + } + + inline void DecrementSleepInhibition() + { + if (!sleepInhibitor) + return; + if (sleepInhibitRefs == 0) + return; + if (--sleepInhibitRefs == 0) + os_inhibit_sleep_set_active(sleepInhibitor, false); + } + + inline void PushUITranslation(obs_frontend_translate_ui_cb cb) { translatorHooks.emplace_front(cb); } + + inline void PopUITranslation() { translatorHooks.pop_front(); } +#ifndef _WIN32 + static void SigIntSignalHandler(int); +#endif + +public slots: + void Exec(VoidFunc func); + void ProcessSigInt(); + +signals: + void StyleChanged(); +}; + +int GetAppConfigPath(char *path, size_t size, const char *name); +char *GetAppConfigPathPtr(const char *name); + +int GetProgramDataPath(char *path, size_t size, const char *name); +char *GetProgramDataPathPtr(const char *name); + +inline OBSApp *App() +{ + return static_cast(qApp); +} + +std::vector> GetLocaleNames(); +inline const char *Str(const char *lookup) +{ + return App()->GetString(lookup); +} +inline QString QTStr(const char *lookupVal) +{ + return QString::fromUtf8(Str(lookupVal)); +} + +bool GetFileSafeName(const char *name, std::string &file); +bool GetClosestUnusedFileName(std::string &path, const char *extension); +bool GetUnusedSceneCollectionFile(std::string &name, std::string &file); + +bool WindowPositionValid(QRect rect); + +extern bool portable_mode; +extern bool steam; +extern bool safe_mode; +extern bool disable_3p_plugins; + +extern bool opt_start_streaming; +extern bool opt_start_recording; +extern bool opt_start_replaybuffer; +extern bool opt_start_virtualcam; +extern bool opt_minimize_tray; +extern bool opt_studio_mode; +extern bool opt_allow_opengl; +extern bool opt_always_on_top; +extern std::string opt_starting_scene; +extern bool restart; +extern bool restart_safe; + +#ifdef _WIN32 +extern "C" void install_dll_blocklist_hook(void); +extern "C" void log_blocked_dlls(void); +#endif diff --git a/UI/obs-app-theming.hpp b/frontend/utility/OBSTheme.hpp similarity index 100% rename from UI/obs-app-theming.hpp rename to frontend/utility/OBSTheme.hpp diff --git a/frontend/utility/OBSThemeVariable.hpp b/frontend/utility/OBSThemeVariable.hpp new file mode 100644 index 000000000..cbaed7f24 --- /dev/null +++ b/frontend/utility/OBSThemeVariable.hpp @@ -0,0 +1,66 @@ +/****************************************************************************** + Copyright (C) 2023 by Dennis Sädtler + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ + +#pragma once + +#include + +#include + +struct OBSThemeVariable; + +struct OBSTheme { + /* internal name, must be unique */ + QString id; + QString name; + QString author; + QString extends; + + /* First ancestor base theme */ + QString parent; + /* Dependencies from root to direct ancestor */ + QStringList dependencies; + /* File path */ + std::filesystem::path location; + std::filesystem::path filename; /* Filename without extension */ + + bool isDark; + bool isVisible; /* Whether it should be shown to the user */ + bool isBaseTheme; /* Whether it is a "style" or variant */ + bool isHighContrast; /* Whether it is a high-contrast adjustment layer */ +}; + +struct OBSThemeVariable { + enum VariableType { + Color, /* RGB color value*/ + Size, /* Number with suffix denoting size (e.g. px, pt, em) */ + Number, /* Number without suffix */ + String, /* Raw string (e.g. color name, border style, etc.) */ + Alias, /* Points at another variable, value will be the key */ + Calc, /* Simple calculation with two operands */ + }; + + /* Whether the variable should be editable in the UI */ + bool editable = false; + /* Used for VariableType::Size only */ + QString suffix; + + VariableType type; + QString name; + QVariant value; + QVariant userValue; /* If overwritten by user, use this value instead */ +}; diff --git a/frontend/utility/OBSTranslator.cpp b/frontend/utility/OBSTranslator.cpp new file mode 100644 index 000000000..883bee21a --- /dev/null +++ b/frontend/utility/OBSTranslator.cpp @@ -0,0 +1,2662 @@ +/****************************************************************************** + Copyright (C) 2023 by Lain Bailey + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "obs-app.hpp" +#include "obs-proxy-style.hpp" +#include "log-viewer.hpp" +#include "volume-control.hpp" +#include "window-basic-main.hpp" +#ifdef __APPLE__ +#include "window-permissions.hpp" +#endif +#include "window-basic-settings.hpp" +#include "platform.hpp" + +#include + +#include + +#ifdef _WIN32 +#include +#include +#include +#else +#include +#include +#include +#include +#include +#endif + +#if defined(_WIN32) || defined(ENABLE_SPARKLE_UPDATER) +#include "update/models/branches.hpp" +#endif + +#if !defined(_WIN32) && !defined(__APPLE__) +#include +#include +#endif + +#include + +#include "ui-config.h" + +using namespace std; + +static log_handler_t def_log_handler; + +static string currentLogFile; +static string lastLogFile; +static string lastCrashLogFile; + +bool portable_mode = false; +bool steam = false; +bool safe_mode = false; +bool disable_3p_plugins = false; +bool unclean_shutdown = false; +bool disable_shutdown_check = false; +static bool multi = false; +static bool log_verbose = false; +static bool unfiltered_log = false; +bool opt_start_streaming = false; +bool opt_start_recording = false; +bool opt_studio_mode = false; +bool opt_start_replaybuffer = false; +bool opt_start_virtualcam = false; +bool opt_minimize_tray = false; +bool opt_allow_opengl = false; +bool opt_always_on_top = false; +bool opt_disable_updater = false; +bool opt_disable_missing_files_check = false; +string opt_starting_collection; +string opt_starting_profile; +string opt_starting_scene; + +bool restart = false; +bool restart_safe = false; +QStringList arguments; + +QPointer obsLogViewer; + +#ifndef _WIN32 +int OBSApp::sigintFd[2]; +#endif + +// GPU hint exports for AMD/NVIDIA laptops +#ifdef _MSC_VER +extern "C" __declspec(dllexport) DWORD NvOptimusEnablement = 1; +extern "C" __declspec(dllexport) int AmdPowerXpressRequestHighPerformance = 1; +#endif + +QObject *CreateShortcutFilter() +{ + return new OBSEventFilter([](QObject *obj, QEvent *event) { + auto mouse_event = [](QMouseEvent &event) { + if (!App()->HotkeysEnabledInFocus() && event.button() != Qt::LeftButton) + return true; + + obs_key_combination_t hotkey = {0, OBS_KEY_NONE}; + bool pressed = event.type() == QEvent::MouseButtonPress; + + switch (event.button()) { + case Qt::NoButton: + case Qt::LeftButton: + case Qt::RightButton: + case Qt::AllButtons: + case Qt::MouseButtonMask: + return false; + + case Qt::MiddleButton: + hotkey.key = OBS_KEY_MOUSE3; + break; + +#define MAP_BUTTON(i, j) \ + case Qt::ExtraButton##i: \ + hotkey.key = OBS_KEY_MOUSE##j; \ + break; + MAP_BUTTON(1, 4); + MAP_BUTTON(2, 5); + MAP_BUTTON(3, 6); + MAP_BUTTON(4, 7); + MAP_BUTTON(5, 8); + MAP_BUTTON(6, 9); + MAP_BUTTON(7, 10); + MAP_BUTTON(8, 11); + MAP_BUTTON(9, 12); + MAP_BUTTON(10, 13); + MAP_BUTTON(11, 14); + MAP_BUTTON(12, 15); + MAP_BUTTON(13, 16); + MAP_BUTTON(14, 17); + MAP_BUTTON(15, 18); + MAP_BUTTON(16, 19); + MAP_BUTTON(17, 20); + MAP_BUTTON(18, 21); + MAP_BUTTON(19, 22); + MAP_BUTTON(20, 23); + MAP_BUTTON(21, 24); + MAP_BUTTON(22, 25); + MAP_BUTTON(23, 26); + MAP_BUTTON(24, 27); +#undef MAP_BUTTON + } + + hotkey.modifiers = TranslateQtKeyboardEventModifiers(event.modifiers()); + + obs_hotkey_inject_event(hotkey, pressed); + return true; + }; + + auto key_event = [&](QKeyEvent *event) { + int key = event->key(); + bool enabledInFocus = App()->HotkeysEnabledInFocus(); + + if (key != Qt::Key_Enter && key != Qt::Key_Escape && key != Qt::Key_Return && !enabledInFocus) + return true; + + QDialog *dialog = qobject_cast(obj); + + obs_key_combination_t hotkey = {0, OBS_KEY_NONE}; + bool pressed = event->type() == QEvent::KeyPress; + + switch (key) { + case Qt::Key_Shift: + case Qt::Key_Control: + case Qt::Key_Alt: + case Qt::Key_Meta: + break; + +#ifdef __APPLE__ + case Qt::Key_CapsLock: + // kVK_CapsLock == 57 + hotkey.key = obs_key_from_virtual_key(57); + pressed = true; + break; +#endif + + case Qt::Key_Enter: + case Qt::Key_Escape: + case Qt::Key_Return: + if (dialog && pressed) + return false; + if (!enabledInFocus) + return true; + /* Falls through. */ + default: + hotkey.key = obs_key_from_virtual_key(event->nativeVirtualKey()); + } + + if (event->isAutoRepeat()) + return true; + + hotkey.modifiers = TranslateQtKeyboardEventModifiers(event->modifiers()); + + obs_hotkey_inject_event(hotkey, pressed); + return true; + }; + + switch (event->type()) { + case QEvent::MouseButtonPress: + case QEvent::MouseButtonRelease: + return mouse_event(*static_cast(event)); + + /*case QEvent::MouseButtonDblClick: + case QEvent::Wheel:*/ + case QEvent::KeyPress: + case QEvent::KeyRelease: + return key_event(static_cast(event)); + + default: + return false; + } + }); +} + +string CurrentTimeString() +{ + using namespace std::chrono; + + struct tm tstruct; + char buf[80]; + + auto tp = system_clock::now(); + auto now = system_clock::to_time_t(tp); + tstruct = *localtime(&now); + + size_t written = strftime(buf, sizeof(buf), "%T", &tstruct); + if (ratio_less::value && written && (sizeof(buf) - written) > 5) { + auto tp_secs = time_point_cast(tp); + auto millis = duration_cast(tp - tp_secs).count(); + + snprintf(buf + written, sizeof(buf) - written, ".%03u", static_cast(millis)); + } + + return buf; +} + +string CurrentDateTimeString() +{ + time_t now = time(0); + struct tm tstruct; + char buf[80]; + tstruct = *localtime(&now); + strftime(buf, sizeof(buf), "%Y-%m-%d, %X", &tstruct); + return buf; +} + +static void LogString(fstream &logFile, const char *timeString, char *str, int log_level) +{ + static mutex logfile_mutex; + string msg; + msg += timeString; + msg += str; + + logfile_mutex.lock(); + logFile << msg << endl; + logfile_mutex.unlock(); + + if (!!obsLogViewer) + QMetaObject::invokeMethod(obsLogViewer.data(), "AddLine", Qt::QueuedConnection, Q_ARG(int, log_level), + Q_ARG(QString, QString(msg.c_str()))); +} + +static inline void LogStringChunk(fstream &logFile, char *str, int log_level) +{ + char *nextLine = str; + string timeString = CurrentTimeString(); + timeString += ": "; + + while (*nextLine) { + char *nextLine = strchr(str, '\n'); + if (!nextLine) + break; + + if (nextLine != str && nextLine[-1] == '\r') { + nextLine[-1] = 0; + } else { + nextLine[0] = 0; + } + + LogString(logFile, timeString.c_str(), str, log_level); + nextLine++; + str = nextLine; + } + + LogString(logFile, timeString.c_str(), str, log_level); +} + +#define MAX_REPEATED_LINES 30 +#define MAX_CHAR_VARIATION (255 * 3) + +static inline int sum_chars(const char *str) +{ + int val = 0; + for (; *str != 0; str++) + val += *str; + + return val; +} + +static inline bool too_many_repeated_entries(fstream &logFile, const char *msg, const char *output_str) +{ + static mutex log_mutex; + static const char *last_msg_ptr = nullptr; + static int last_char_sum = 0; + static int rep_count = 0; + + int new_sum = sum_chars(output_str); + + lock_guard guard(log_mutex); + + if (unfiltered_log) { + return false; + } + + if (last_msg_ptr == msg) { + int diff = std::abs(new_sum - last_char_sum); + if (diff < MAX_CHAR_VARIATION) { + return (rep_count++ >= MAX_REPEATED_LINES); + } + } + + if (rep_count > MAX_REPEATED_LINES) { + logFile << CurrentTimeString() << ": Last log entry repeated for " + << to_string(rep_count - MAX_REPEATED_LINES) << " more lines" << endl; + } + + last_msg_ptr = msg; + last_char_sum = new_sum; + rep_count = 0; + + return false; +} + +static void do_log(int log_level, const char *msg, va_list args, void *param) +{ + fstream &logFile = *static_cast(param); + char str[8192]; + +#ifndef _WIN32 + va_list args2; + va_copy(args2, args); +#endif + + vsnprintf(str, sizeof(str), msg, args); + +#ifdef _WIN32 + if (IsDebuggerPresent()) { + int wNum = MultiByteToWideChar(CP_UTF8, 0, str, -1, NULL, 0); + if (wNum > 1) { + static wstring wide_buf; + static mutex wide_mutex; + + lock_guard lock(wide_mutex); + wide_buf.reserve(wNum + 1); + wide_buf.resize(wNum - 1); + MultiByteToWideChar(CP_UTF8, 0, str, -1, &wide_buf[0], wNum); + wide_buf.push_back('\n'); + + OutputDebugStringW(wide_buf.c_str()); + } + } +#endif + +#if !defined(_WIN32) && defined(_DEBUG) + def_log_handler(log_level, msg, args2, nullptr); +#endif + + if (log_level <= LOG_INFO || log_verbose) { +#if !defined(_WIN32) && !defined(_DEBUG) + def_log_handler(log_level, msg, args2, nullptr); +#endif + if (!too_many_repeated_entries(logFile, msg, str)) + LogStringChunk(logFile, str, log_level); + } + +#if defined(_WIN32) && defined(OBS_DEBUGBREAK_ON_ERROR) + if (log_level <= LOG_ERROR && IsDebuggerPresent()) + __debugbreak(); +#endif + +#ifndef _WIN32 + va_end(args2); +#endif +} + +#define DEFAULT_LANG "en-US" + +bool OBSApp::InitGlobalConfigDefaults() +{ + config_set_default_uint(appConfig, "General", "MaxLogs", 10); + config_set_default_int(appConfig, "General", "InfoIncrement", -1); + config_set_default_string(appConfig, "General", "ProcessPriority", "Normal"); + config_set_default_bool(appConfig, "General", "EnableAutoUpdates", true); + +#if _WIN32 + config_set_default_string(appConfig, "Video", "Renderer", "Direct3D 11"); +#else + config_set_default_string(appConfig, "Video", "Renderer", "OpenGL"); +#endif + +#ifdef _WIN32 + config_set_default_bool(appConfig, "Audio", "DisableAudioDucking", true); + config_set_default_bool(appConfig, "General", "BrowserHWAccel", true); +#endif + +#ifdef __APPLE__ + config_set_default_bool(appConfig, "General", "BrowserHWAccel", true); + config_set_default_bool(appConfig, "Video", "DisableOSXVSync", true); + config_set_default_bool(appConfig, "Video", "ResetOSXVSyncOnExit", true); +#endif + + return true; +} + +bool OBSApp::InitGlobalLocationDefaults() +{ + char path[512]; + + int len = GetAppConfigPath(path, sizeof(path), nullptr); + if (len <= 0) { + OBSErrorBox(NULL, "Unable to get global configuration path."); + return false; + } + + config_set_default_string(appConfig, "Locations", "Configuration", path); + config_set_default_string(appConfig, "Locations", "SceneCollections", path); + config_set_default_string(appConfig, "Locations", "Profiles", path); + + return true; +} + +void OBSApp::InitUserConfigDefaults() +{ + config_set_default_bool(userConfig, "General", "ConfirmOnExit", true); + + config_set_default_string(userConfig, "General", "HotkeyFocusType", "NeverDisableHotkeys"); + + config_set_default_bool(userConfig, "BasicWindow", "PreviewEnabled", true); + config_set_default_bool(userConfig, "BasicWindow", "PreviewProgramMode", false); + config_set_default_bool(userConfig, "BasicWindow", "SceneDuplicationMode", true); + config_set_default_bool(userConfig, "BasicWindow", "SwapScenesMode", true); + config_set_default_bool(userConfig, "BasicWindow", "SnappingEnabled", true); + config_set_default_bool(userConfig, "BasicWindow", "ScreenSnapping", true); + config_set_default_bool(userConfig, "BasicWindow", "SourceSnapping", true); + config_set_default_bool(userConfig, "BasicWindow", "CenterSnapping", false); + config_set_default_double(userConfig, "BasicWindow", "SnapDistance", 10.0); + config_set_default_bool(userConfig, "BasicWindow", "SpacingHelpersEnabled", true); + config_set_default_bool(userConfig, "BasicWindow", "RecordWhenStreaming", false); + config_set_default_bool(userConfig, "BasicWindow", "KeepRecordingWhenStreamStops", false); + config_set_default_bool(userConfig, "BasicWindow", "SysTrayEnabled", true); + config_set_default_bool(userConfig, "BasicWindow", "SysTrayWhenStarted", false); + config_set_default_bool(userConfig, "BasicWindow", "SaveProjectors", false); + config_set_default_bool(userConfig, "BasicWindow", "ShowTransitions", true); + config_set_default_bool(userConfig, "BasicWindow", "ShowListboxToolbars", true); + config_set_default_bool(userConfig, "BasicWindow", "ShowStatusBar", true); + config_set_default_bool(userConfig, "BasicWindow", "ShowSourceIcons", true); + config_set_default_bool(userConfig, "BasicWindow", "ShowContextToolbars", true); + config_set_default_bool(userConfig, "BasicWindow", "StudioModeLabels", true); + + config_set_default_bool(userConfig, "BasicWindow", "VerticalVolControl", false); + + config_set_default_bool(userConfig, "BasicWindow", "MultiviewMouseSwitch", true); + + config_set_default_bool(userConfig, "BasicWindow", "MultiviewDrawNames", true); + + config_set_default_bool(userConfig, "BasicWindow", "MultiviewDrawAreas", true); + + config_set_default_bool(userConfig, "BasicWindow", "MediaControlsCountdownTimer", true); +} + +static bool do_mkdir(const char *path) +{ + if (os_mkdirs(path) == MKDIR_ERROR) { + OBSErrorBox(NULL, "Failed to create directory %s", path); + return false; + } + + return true; +} + +static bool MakeUserDirs() +{ + char path[512]; + + if (GetAppConfigPath(path, sizeof(path), "obs-studio/basic") <= 0) + return false; + if (!do_mkdir(path)) + return false; + + if (GetAppConfigPath(path, sizeof(path), "obs-studio/logs") <= 0) + return false; + if (!do_mkdir(path)) + return false; + + if (GetAppConfigPath(path, sizeof(path), "obs-studio/profiler_data") <= 0) + return false; + if (!do_mkdir(path)) + return false; + +#ifdef _WIN32 + if (GetAppConfigPath(path, sizeof(path), "obs-studio/crashes") <= 0) + return false; + if (!do_mkdir(path)) + return false; +#endif + +#ifdef WHATSNEW_ENABLED + if (GetAppConfigPath(path, sizeof(path), "obs-studio/updates") <= 0) + return false; + if (!do_mkdir(path)) + return false; +#endif + + if (GetAppConfigPath(path, sizeof(path), "obs-studio/plugin_config") <= 0) + return false; + if (!do_mkdir(path)) + return false; + + return true; +} + +constexpr std::string_view OBSProfileSubDirectory = "obs-studio/basic/profiles"; +constexpr std::string_view OBSScenesSubDirectory = "obs-studio/basic/scenes"; + +static bool MakeUserProfileDirs() +{ + const std::filesystem::path userProfilePath = + App()->userProfilesLocation / std::filesystem::u8path(OBSProfileSubDirectory); + const std::filesystem::path userScenesPath = + App()->userScenesLocation / std::filesystem::u8path(OBSScenesSubDirectory); + + 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 (!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; +} + +bool OBSApp::UpdatePre22MultiviewLayout(const char *layout) +{ + if (!layout) + return false; + + if (astrcmpi(layout, "horizontaltop") == 0) { + config_set_int(userConfig, "BasicWindow", "MultiviewLayout", + static_cast(MultiviewLayout::HORIZONTAL_TOP_8_SCENES)); + return true; + } + + if (astrcmpi(layout, "horizontalbottom") == 0) { + config_set_int(userConfig, "BasicWindow", "MultiviewLayout", + static_cast(MultiviewLayout::HORIZONTAL_BOTTOM_8_SCENES)); + return true; + } + + if (astrcmpi(layout, "verticalleft") == 0) { + config_set_int(userConfig, "BasicWindow", "MultiviewLayout", + static_cast(MultiviewLayout::VERTICAL_LEFT_8_SCENES)); + return true; + } + + if (astrcmpi(layout, "verticalright") == 0) { + config_set_int(userConfig, "BasicWindow", "MultiviewLayout", + static_cast(MultiviewLayout::VERTICAL_RIGHT_8_SCENES)); + return true; + } + + return false; +} + +bool OBSApp::InitGlobalConfig() +{ + char path[512]; + + int len = GetAppConfigPath(path, sizeof(path), "obs-studio/global.ini"); + if (len <= 0) { + return false; + } + + int errorcode = appConfig.Open(path, CONFIG_OPEN_ALWAYS); + if (errorcode != CONFIG_SUCCESS) { + OBSErrorBox(NULL, "Failed to open global.ini: %d", errorcode); + return false; + } + + uint32_t lastVersion = config_get_int(appConfig, "General", "LastVersion"); + + if (lastVersion < MAKE_SEMANTIC_VERSION(31, 0, 0)) { + bool migratedUserSettings = config_get_bool(appConfig, "General", "Pre31Migrated"); + + if (!migratedUserSettings) { + bool migrated = MigrateGlobalSettings(); + + config_set_bool(appConfig, "General", "Pre31Migrated", migrated); + config_save_safe(appConfig, "tmp", nullptr); + } + } + + InitGlobalConfigDefaults(); + InitGlobalLocationDefaults(); + + if (IsPortableMode()) { + userConfigLocation = + std::filesystem::u8path(config_get_default_string(appConfig, "Locations", "Configuration")); + userScenesLocation = + std::filesystem::u8path(config_get_default_string(appConfig, "Locations", "SceneCollections")); + userProfilesLocation = + std::filesystem::u8path(config_get_default_string(appConfig, "Locations", "Profiles")); + } else { + userConfigLocation = + std::filesystem::u8path(config_get_string(appConfig, "Locations", "Configuration")); + userScenesLocation = + std::filesystem::u8path(config_get_string(appConfig, "Locations", "SceneCollections")); + userProfilesLocation = std::filesystem::u8path(config_get_string(appConfig, "Locations", "Profiles")); + } + + bool userConfigResult = InitUserConfig(userConfigLocation, lastVersion); + + return userConfigResult; +} + +bool OBSApp::InitUserConfig(std::filesystem::path &userConfigLocation, uint32_t lastVersion) +{ + const std::string userConfigFile = userConfigLocation.u8string() + "/obs-studio/user.ini"; + + int errorCode = userConfig.Open(userConfigFile.c_str(), CONFIG_OPEN_ALWAYS); + + if (errorCode != CONFIG_SUCCESS) { + OBSErrorBox(nullptr, "Failed to open user.ini: %d", errorCode); + return false; + } + + MigrateLegacySettings(lastVersion); + InitUserConfigDefaults(); + + return true; +} + +void OBSApp::MigrateLegacySettings(const uint32_t lastVersion) +{ + bool hasChanges = false; + + const uint32_t v19 = MAKE_SEMANTIC_VERSION(19, 0, 0); + const uint32_t v21 = MAKE_SEMANTIC_VERSION(21, 0, 0); + const uint32_t v23 = MAKE_SEMANTIC_VERSION(23, 0, 0); + const uint32_t v24 = MAKE_SEMANTIC_VERSION(24, 0, 0); + const uint32_t v24_1 = MAKE_SEMANTIC_VERSION(24, 1, 0); + + const map defaultsMap{ + {{v19, "Pre19Defaults"}, {v21, "Pre21Defaults"}, {v23, "Pre23Defaults"}, {v24_1, "Pre24.1Defaults"}}}; + + for (auto &[version, configKey] : defaultsMap) { + if (!config_has_user_value(userConfig, "General", configKey.c_str())) { + bool useOldDefaults = lastVersion && lastVersion < version; + config_set_bool(userConfig, "General", configKey.c_str(), useOldDefaults); + + hasChanges = true; + } + } + + if (config_has_user_value(userConfig, "BasicWindow", "MultiviewLayout")) { + const char *layout = config_get_string(userConfig, "BasicWindow", "MultiviewLayout"); + + bool layoutUpdated = UpdatePre22MultiviewLayout(layout); + + hasChanges = hasChanges | layoutUpdated; + } + + if (lastVersion && lastVersion < v24) { + bool disableHotkeysInFocus = config_get_bool(userConfig, "General", "DisableHotkeysInFocus"); + + if (disableHotkeysInFocus) { + config_set_string(userConfig, "General", "HotkeyFocusType", "DisableHotkeysInFocus"); + } + + hasChanges = true; + } + + if (hasChanges) { + userConfig.SaveSafe("tmp"); + } +} + +static constexpr string_view OBSGlobalIniPath = "/obs-studio/global.ini"; +static constexpr string_view OBSUserIniPath = "/obs-studio/user.ini"; + +bool OBSApp::MigrateGlobalSettings() +{ + char path[512]; + + int len = GetAppConfigPath(path, sizeof(path), nullptr); + if (len <= 0) { + OBSErrorBox(nullptr, "Unable to get global configuration path."); + return false; + } + + std::string legacyConfigFileString; + legacyConfigFileString.reserve(strlen(path) + OBSGlobalIniPath.size()); + legacyConfigFileString.append(path).append(OBSGlobalIniPath); + + const std::filesystem::path legacyGlobalConfigFile = std::filesystem::u8path(legacyConfigFileString); + + std::string configFileString; + configFileString.reserve(strlen(path) + OBSUserIniPath.size()); + configFileString.append(path).append(OBSUserIniPath); + + const std::filesystem::path userConfigFile = std::filesystem::u8path(configFileString); + + if (std::filesystem::exists(userConfigFile)) { + OBSErrorBox(nullptr, + "Unable to migrate global configuration - user configuration file already exists."); + return false; + } + + try { + std::filesystem::copy(legacyGlobalConfigFile, userConfigFile); + } catch (const std::filesystem::filesystem_error &) { + OBSErrorBox(nullptr, "Unable to migrate global configuration - copy failed."); + return false; + } + + return true; +} + +bool OBSApp::InitLocale() +{ + ProfileScope("OBSApp::InitLocale"); + + const char *lang = config_get_string(userConfig, "General", "Language"); + bool userLocale = config_has_user_value(userConfig, "General", "Language"); + if (!userLocale || !lang || lang[0] == '\0') + lang = DEFAULT_LANG; + + locale = lang; + + // set basic default application locale + if (!locale.empty()) + QLocale::setDefault(QLocale(QString::fromStdString(locale).replace('-', '_'))); + + string englishPath; + if (!GetDataFilePath("locale/" DEFAULT_LANG ".ini", englishPath)) { + OBSErrorBox(NULL, "Failed to find locale/" DEFAULT_LANG ".ini"); + return false; + } + + textLookup = text_lookup_create(englishPath.c_str()); + if (!textLookup) { + OBSErrorBox(NULL, "Failed to create locale from file '%s'", englishPath.c_str()); + return false; + } + + bool defaultLang = astrcmpi(lang, DEFAULT_LANG) == 0; + + if (userLocale && defaultLang) + return true; + + if (!userLocale && defaultLang) { + for (auto &locale_ : GetPreferredLocales()) { + if (locale_ == lang) + return true; + + stringstream file; + file << "locale/" << locale_ << ".ini"; + + string path; + if (!GetDataFilePath(file.str().c_str(), path)) + continue; + + if (!text_lookup_add(textLookup, path.c_str())) + continue; + + blog(LOG_INFO, "Using preferred locale '%s'", locale_.c_str()); + locale = locale_; + + // set application default locale to the new choosen one + if (!locale.empty()) + QLocale::setDefault(QLocale(QString::fromStdString(locale).replace('-', '_'))); + + return true; + } + + return true; + } + + stringstream file; + file << "locale/" << lang << ".ini"; + + string path; + if (GetDataFilePath(file.str().c_str(), path)) { + if (!text_lookup_add(textLookup, path.c_str())) + blog(LOG_ERROR, "Failed to add locale file '%s'", path.c_str()); + } else { + blog(LOG_ERROR, "Could not find locale file '%s'", file.str().c_str()); + } + + return true; +} + +#if defined(_WIN32) || defined(ENABLE_SPARKLE_UPDATER) +void ParseBranchesJson(const std::string &jsonString, vector &out, std::string &error) +{ + JsonBranches branches; + + try { + nlohmann::json json = nlohmann::json::parse(jsonString); + branches = json.get(); + } catch (nlohmann::json::exception &e) { + error = e.what(); + return; + } + + for (const JsonBranch &json_branch : branches) { +#ifdef _WIN32 + if (!json_branch.windows) + continue; +#elif defined(__APPLE__) + if (!json_branch.macos) + continue; +#endif + + UpdateBranch branch = { + QString::fromStdString(json_branch.name), + QString::fromStdString(json_branch.display_name), + QString::fromStdString(json_branch.description), + json_branch.enabled, + json_branch.visible, + }; + out.push_back(branch); + } +} + +bool LoadBranchesFile(vector &out) +{ + string error; + string branchesText; + + BPtr branchesFilePath = GetAppConfigPathPtr("obs-studio/updates/branches.json"); + + QFile branchesFile(branchesFilePath.Get()); + if (!branchesFile.open(QIODevice::ReadOnly)) { + error = "Opening file failed."; + goto fail; + } + + branchesText = branchesFile.readAll(); + if (branchesText.empty()) { + error = "File empty."; + goto fail; + } + + ParseBranchesJson(branchesText, out, error); + if (error.empty()) + return !out.empty(); + +fail: + blog(LOG_WARNING, "Loading branches from file failed: %s", error.c_str()); + return false; +} +#endif + +void OBSApp::SetBranchData(const string &data) +{ +#if defined(_WIN32) || defined(ENABLE_SPARKLE_UPDATER) + string error; + vector result; + + ParseBranchesJson(data, result, error); + + if (!error.empty()) { + blog(LOG_WARNING, "Reading branches JSON response failed: %s", error.c_str()); + return; + } + + if (!result.empty()) + updateBranches = result; + + branches_loaded = true; +#else + UNUSED_PARAMETER(data); +#endif +} + +std::vector OBSApp::GetBranches() +{ + vector out; + /* Always ensure the default branch exists */ + out.push_back(UpdateBranch{"stable", "", "", true, true}); + +#if defined(_WIN32) || defined(ENABLE_SPARKLE_UPDATER) + if (!branches_loaded) { + vector result; + if (LoadBranchesFile(result)) + updateBranches = result; + + branches_loaded = true; + } +#endif + + /* Copy additional branches to result (if any) */ + if (!updateBranches.empty()) + out.insert(out.end(), updateBranches.begin(), updateBranches.end()); + + return out; +} + +OBSApp::OBSApp(int &argc, char **argv, profiler_name_store_t *store) + : QApplication(argc, argv), + profilerNameStore(store) +{ + /* fix float handling */ +#if defined(Q_OS_UNIX) + if (!setlocale(LC_NUMERIC, "C")) + blog(LOG_WARNING, "Failed to set LC_NUMERIC to C locale"); +#endif + +#ifndef _WIN32 + /* Handle SIGINT properly */ + socketpair(AF_UNIX, SOCK_STREAM, 0, sigintFd); + snInt = new QSocketNotifier(sigintFd[1], QSocketNotifier::Read, this); + connect(snInt, &QSocketNotifier::activated, this, &OBSApp::ProcessSigInt); +#else + connect(qApp, &QGuiApplication::commitDataRequest, this, &OBSApp::commitData); +#endif + + sleepInhibitor = os_inhibit_sleep_create("OBS Video/audio"); + +#ifndef __APPLE__ + setWindowIcon(QIcon::fromTheme("obs", QIcon(":/res/images/obs.png"))); +#endif + + setDesktopFileName("com.obsproject.Studio"); +} + +OBSApp::~OBSApp() +{ +#ifdef _WIN32 + bool disableAudioDucking = config_get_bool(appConfig, "Audio", "DisableAudioDucking"); + if (disableAudioDucking) + DisableAudioDucking(false); +#else + delete snInt; + close(sigintFd[0]); + close(sigintFd[1]); +#endif + +#ifdef __APPLE__ + bool vsyncDisabled = config_get_bool(appConfig, "Video", "DisableOSXVSync"); + bool resetVSync = config_get_bool(appConfig, "Video", "ResetOSXVSyncOnExit"); + if (vsyncDisabled && resetVSync) + EnableOSXVSync(true); +#endif + + os_inhibit_sleep_set_active(sleepInhibitor, false); + os_inhibit_sleep_destroy(sleepInhibitor); + + if (libobs_initialized) + obs_shutdown(); +} + +static void move_basic_to_profiles(void) +{ + char path[512]; + + if (GetAppConfigPath(path, 512, "obs-studio/basic") <= 0) { + return; + } + + 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) +{ + char path[512]; + + if (GetAppConfigPath(path, 512, "obs-studio/basic") <= 0) { + return; + } + + const std::filesystem::path basicPath = std::filesystem::u8path(path); + + if (!std::filesystem::exists(basicPath)) { + return; + } + + const std::filesystem::path sceneCollectionPath = + App()->userScenesLocation / std::filesystem::u8path("obs-studio/basic/scenes"); + + if (std::filesystem::exists(sceneCollectionPath)) { + return; + } + + try { + std::filesystem::create_directories(sceneCollectionPath); + } catch (const std::filesystem::filesystem_error &error) { + blog(LOG_ERROR, + "Failed to create scene collection directory for migration from basic scene collection\n%s", + error.what()); + return; + } + + const std::filesystem::path sourceFile = basicPath / std::filesystem::u8path("scenes.json"); + const std::filesystem::path destinationFile = + (sceneCollectionPath / std::filesystem::u8path(Str("Untitled"))).replace_extension(".json"); + + try { + std::filesystem::rename(sourceFile, destinationFile); + } catch (const std::filesystem::filesystem_error &error) { + blog(LOG_ERROR, "Failed to rename basic scene collection file:\n%s", error.what()); + return; + } +} + +void OBSApp::AppInit() +{ + ProfileScope("OBSApp::AppInit"); + + if (!MakeUserDirs()) + throw "Failed to create required user directories"; + if (!InitGlobalConfig()) + throw "Failed to initialize global config"; + if (!InitLocale()) + throw "Failed to load locale"; + if (!InitTheme()) + throw "Failed to load theme"; + + config_set_default_string(userConfig, "Basic", "Profile", Str("Untitled")); + config_set_default_string(userConfig, "Basic", "ProfileDir", Str("Untitled")); + config_set_default_string(userConfig, "Basic", "SceneCollection", Str("Untitled")); + config_set_default_string(userConfig, "Basic", "SceneCollectionFile", Str("Untitled")); + config_set_default_bool(userConfig, "Basic", "ConfigOnNewProfile", true); + + if (!config_has_user_value(userConfig, "Basic", "Profile")) { + config_set_string(userConfig, "Basic", "Profile", Str("Untitled")); + config_set_string(userConfig, "Basic", "ProfileDir", Str("Untitled")); + } + + if (!config_has_user_value(userConfig, "Basic", "SceneCollection")) { + config_set_string(userConfig, "Basic", "SceneCollection", Str("Untitled")); + config_set_string(userConfig, "Basic", "SceneCollectionFile", Str("Untitled")); + } + +#ifdef _WIN32 + bool disableAudioDucking = config_get_bool(appConfig, "Audio", "DisableAudioDucking"); + if (disableAudioDucking) + DisableAudioDucking(true); +#endif + +#ifdef __APPLE__ + if (config_get_bool(appConfig, "Video", "DisableOSXVSync")) + EnableOSXVSync(false); +#endif + + UpdateHotkeyFocusSetting(false); + + move_basic_to_profiles(); + move_basic_to_scene_collections(); + + if (!MakeUserProfileDirs()) + throw "Failed to create profile directories"; +} + +const char *OBSApp::GetRenderModule() const +{ + const char *renderer = config_get_string(appConfig, "Video", "Renderer"); + + return (astrcmpi(renderer, "Direct3D 11") == 0) ? DL_D3D11 : DL_OPENGL; +} + +static bool StartupOBS(const char *locale, profiler_name_store_t *store) +{ + char path[512]; + + if (GetAppConfigPath(path, sizeof(path), "obs-studio/plugin_config") <= 0) + return false; + + return obs_startup(locale, path, store); +} + +inline void OBSApp::ResetHotkeyState(bool inFocus) +{ + obs_hotkey_enable_background_press((inFocus && enableHotkeysInFocus) || (!inFocus && enableHotkeysOutOfFocus)); +} + +void OBSApp::UpdateHotkeyFocusSetting(bool resetState) +{ + enableHotkeysInFocus = true; + enableHotkeysOutOfFocus = true; + + const char *hotkeyFocusType = config_get_string(userConfig, "General", "HotkeyFocusType"); + + if (astrcmpi(hotkeyFocusType, "DisableHotkeysInFocus") == 0) { + enableHotkeysInFocus = false; + } else if (astrcmpi(hotkeyFocusType, "DisableHotkeysOutOfFocus") == 0) { + enableHotkeysOutOfFocus = false; + } + + if (resetState) + ResetHotkeyState(applicationState() == Qt::ApplicationActive); +} + +void OBSApp::DisableHotkeys() +{ + enableHotkeysInFocus = false; + enableHotkeysOutOfFocus = false; + ResetHotkeyState(applicationState() == Qt::ApplicationActive); +} + +Q_DECLARE_METATYPE(VoidFunc) + +void OBSApp::Exec(VoidFunc func) +{ + func(); +} + +static void ui_task_handler(obs_task_t task, void *param, bool wait) +{ + auto doTask = [=]() { + /* to get clang-format to behave */ + task(param); + }; + QMetaObject::invokeMethod(App(), "Exec", wait ? WaitConnection() : Qt::AutoConnection, Q_ARG(VoidFunc, doTask)); +} + +bool OBSApp::OBSInit() +{ + ProfileScope("OBSApp::OBSInit"); + + qRegisterMetaType("VoidFunc"); + +#if !defined(_WIN32) && !defined(__APPLE__) + if (QApplication::platformName() == "xcb") { + obs_set_nix_platform(OBS_NIX_PLATFORM_X11_EGL); + blog(LOG_INFO, "Using EGL/X11"); + } + +#ifdef ENABLE_WAYLAND + if (QApplication::platformName().contains("wayland")) { + obs_set_nix_platform(OBS_NIX_PLATFORM_WAYLAND); + setAttribute(Qt::AA_DontCreateNativeWidgetSiblings); + blog(LOG_INFO, "Platform: Wayland"); + } +#endif + + QPlatformNativeInterface *native = QGuiApplication::platformNativeInterface(); + obs_set_nix_platform_display(native->nativeResourceForIntegration("display")); +#endif + +#ifdef __APPLE__ + setAttribute(Qt::AA_DontCreateNativeWidgetSiblings); +#endif + + if (!StartupOBS(locale.c_str(), GetProfilerNameStore())) + return false; + + libobs_initialized = true; + + obs_set_ui_task_handler(ui_task_handler); + +#if defined(_WIN32) || defined(__APPLE__) + bool browserHWAccel = config_get_bool(appConfig, "General", "BrowserHWAccel"); + + OBSDataAutoRelease settings = obs_data_create(); + obs_data_set_bool(settings, "BrowserHWAccel", browserHWAccel); + obs_apply_private_data(settings); + + blog(LOG_INFO, "Current Date/Time: %s", CurrentDateTimeString().c_str()); + + blog(LOG_INFO, "Browser Hardware Acceleration: %s", browserHWAccel ? "true" : "false"); +#endif +#ifdef _WIN32 + bool hideFromCapture = config_get_bool(userConfig, "BasicWindow", "HideOBSWindowsFromCapture"); + blog(LOG_INFO, "Hide OBS windows from screen capture: %s", hideFromCapture ? "true" : "false"); +#endif + + blog(LOG_INFO, "Qt Version: %s (runtime), %s (compiled)", qVersion(), QT_VERSION_STR); + blog(LOG_INFO, "Portable mode: %s", portable_mode ? "true" : "false"); + + if (safe_mode) { + blog(LOG_WARNING, "Safe Mode enabled."); + } else if (disable_3p_plugins) { + blog(LOG_WARNING, "Third-party plugins disabled."); + } + + setQuitOnLastWindowClosed(false); + + mainWindow = new OBSBasic(); + + mainWindow->setAttribute(Qt::WA_DeleteOnClose, true); + connect(mainWindow, &OBSBasic::destroyed, this, &OBSApp::quit); + + mainWindow->OBSInit(); + + connect(this, &QGuiApplication::applicationStateChanged, + [this](Qt::ApplicationState state) { ResetHotkeyState(state == Qt::ApplicationActive); }); + ResetHotkeyState(applicationState() == Qt::ApplicationActive); + return true; +} + +string OBSApp::GetVersionString(bool platform) const +{ + stringstream ver; + +#ifdef HAVE_OBSCONFIG_H + ver << obs_get_version_string(); +#else + ver << LIBOBS_API_MAJOR_VER << "." << LIBOBS_API_MINOR_VER << "." << LIBOBS_API_PATCH_VER; + +#endif + + if (platform) { + ver << " ("; +#ifdef _WIN32 + if (sizeof(void *) == 8) + ver << "64-bit, "; + else + ver << "32-bit, "; + + ver << "windows)"; +#elif __APPLE__ + ver << "mac)"; +#elif __OpenBSD__ + ver << "openbsd)"; +#elif __FreeBSD__ + ver << "freebsd)"; +#else /* assume linux for the time being */ + ver << "linux)"; +#endif + } + + return ver.str(); +} + +bool OBSApp::IsPortableMode() +{ + return portable_mode; +} + +bool OBSApp::IsUpdaterDisabled() +{ + return opt_disable_updater; +} + +bool OBSApp::IsMissingFilesCheckDisabled() +{ + return opt_disable_missing_files_check; +} + +#ifdef __APPLE__ +#define INPUT_AUDIO_SOURCE "coreaudio_input_capture" +#define OUTPUT_AUDIO_SOURCE "coreaudio_output_capture" +#elif _WIN32 +#define INPUT_AUDIO_SOURCE "wasapi_input_capture" +#define OUTPUT_AUDIO_SOURCE "wasapi_output_capture" +#else +#define INPUT_AUDIO_SOURCE "pulse_input_capture" +#define OUTPUT_AUDIO_SOURCE "pulse_output_capture" +#endif + +const char *OBSApp::InputAudioSource() const +{ + return INPUT_AUDIO_SOURCE; +} + +const char *OBSApp::OutputAudioSource() const +{ + return OUTPUT_AUDIO_SOURCE; +} + +const char *OBSApp::GetLastLog() const +{ + return lastLogFile.c_str(); +} + +const char *OBSApp::GetCurrentLog() const +{ + return currentLogFile.c_str(); +} + +const char *OBSApp::GetLastCrashLog() const +{ + return lastCrashLogFile.c_str(); +} + +bool OBSApp::TranslateString(const char *lookupVal, const char **out) const +{ + for (obs_frontend_translate_ui_cb cb : translatorHooks) { + if (cb(lookupVal, out)) + return true; + } + + return text_lookup_getstr(App()->GetTextLookup(), lookupVal, out); +} + +// Global handler to receive all QEvent::Show events so we can apply +// display affinity on any newly created windows and dialogs without +// caring where they are coming from (e.g. plugins). +bool OBSApp::notify(QObject *receiver, QEvent *e) +{ + QWidget *w; + QWindow *window; + int windowType; + + if (!receiver->isWidgetType()) + goto skip; + + if (e->type() != QEvent::Show) + goto skip; + + w = qobject_cast(receiver); + + if (!w->isWindow()) + goto skip; + + window = w->windowHandle(); + if (!window) + goto skip; + + windowType = window->flags() & Qt::WindowType::WindowType_Mask; + + if (windowType == Qt::WindowType::Dialog || windowType == Qt::WindowType::Window || + windowType == Qt::WindowType::Tool) { + OBSBasic *main = reinterpret_cast(GetMainWindow()); + if (main) + main->SetDisplayAffinity(window); + } + +skip: + return QApplication::notify(receiver, e); +} + +QString OBSTranslator::translate(const char *, const char *sourceText, const char *, int) const +{ + const char *out = nullptr; + QString str(sourceText); + str.replace(" ", ""); + if (!App()->TranslateString(QT_TO_UTF8(str), &out)) + return QString(sourceText); + + return QT_UTF8(out); +} + +static bool get_token(lexer *lex, string &str, base_token_type type) +{ + base_token token; + if (!lexer_getbasetoken(lex, &token, IGNORE_WHITESPACE)) + return false; + if (token.type != type) + return false; + + str.assign(token.text.array, token.text.len); + return true; +} + +static bool expect_token(lexer *lex, const char *str, base_token_type type) +{ + base_token token; + if (!lexer_getbasetoken(lex, &token, IGNORE_WHITESPACE)) + return false; + if (token.type != type) + return false; + + return strref_cmp(&token.text, str) == 0; +} + +static uint64_t convert_log_name(bool has_prefix, const char *name) +{ + BaseLexer lex; + string year, month, day, hour, minute, second; + + lexer_start(lex, name); + + if (has_prefix) { + string temp; + if (!get_token(lex, temp, BASETOKEN_ALPHA)) + return 0; + } + + if (!get_token(lex, year, BASETOKEN_DIGIT)) + return 0; + if (!expect_token(lex, "-", BASETOKEN_OTHER)) + return 0; + if (!get_token(lex, month, BASETOKEN_DIGIT)) + return 0; + if (!expect_token(lex, "-", BASETOKEN_OTHER)) + return 0; + if (!get_token(lex, day, BASETOKEN_DIGIT)) + return 0; + if (!get_token(lex, hour, BASETOKEN_DIGIT)) + return 0; + if (!expect_token(lex, "-", BASETOKEN_OTHER)) + return 0; + if (!get_token(lex, minute, BASETOKEN_DIGIT)) + return 0; + if (!expect_token(lex, "-", BASETOKEN_OTHER)) + return 0; + if (!get_token(lex, second, BASETOKEN_DIGIT)) + return 0; + + stringstream timestring; + timestring << year << month << day << hour << minute << second; + return std::stoull(timestring.str()); +} + +/* If upgrading from an older (non-XDG) build of OBS, move config files to XDG directory. */ +/* TODO: Remove after version 32.0. */ +#if defined(__FreeBSD__) +static void move_to_xdg(void) +{ + char old_path[512]; + char new_path[512]; + char *home = getenv("HOME"); + if (!home) + return; + + if (snprintf(old_path, sizeof(old_path), "%s/.obs-studio", home) <= 0) + return; + + /* make base xdg path if it doesn't already exist */ + if (GetAppConfigPath(new_path, sizeof(new_path), "") <= 0) + return; + if (os_mkdirs(new_path) == MKDIR_ERROR) + return; + + if (GetAppConfigPath(new_path, sizeof(new_path), "obs-studio") <= 0) + return; + + if (os_file_exists(old_path) && !os_file_exists(new_path)) { + rename(old_path, new_path); + } +} +#endif + +static void delete_oldest_file(bool has_prefix, const char *location) +{ + BPtr logDir(GetAppConfigPathPtr(location)); + string oldestLog; + uint64_t oldest_ts = (uint64_t)-1; + struct os_dirent *entry; + + unsigned int maxLogs = (unsigned int)config_get_uint(App()->GetAppConfig(), "General", "MaxLogs"); + + os_dir_t *dir = os_opendir(logDir); + if (dir) { + unsigned int count = 0; + + while ((entry = os_readdir(dir)) != NULL) { + if (entry->directory || *entry->d_name == '.') + continue; + + uint64_t ts = convert_log_name(has_prefix, entry->d_name); + + if (ts) { + if (ts < oldest_ts) { + oldestLog = entry->d_name; + oldest_ts = ts; + } + + count++; + } + } + + os_closedir(dir); + + if (count > maxLogs) { + stringstream delPath; + + delPath << logDir << "/" << oldestLog; + os_unlink(delPath.str().c_str()); + } + } +} + +static void get_last_log(bool has_prefix, const char *subdir_to_use, std::string &last) +{ + BPtr logDir(GetAppConfigPathPtr(subdir_to_use)); + struct os_dirent *entry; + os_dir_t *dir = os_opendir(logDir); + uint64_t highest_ts = 0; + + if (dir) { + while ((entry = os_readdir(dir)) != NULL) { + if (entry->directory || *entry->d_name == '.') + continue; + + uint64_t ts = convert_log_name(has_prefix, entry->d_name); + + if (ts > highest_ts) { + last = entry->d_name; + highest_ts = ts; + } + } + + os_closedir(dir); + } +} + +string GenerateTimeDateFilename(const char *extension, bool noSpace) +{ + time_t now = time(0); + char file[256] = {}; + struct tm *cur_time; + + cur_time = localtime(&now); + snprintf(file, sizeof(file), "%d-%02d-%02d%c%02d-%02d-%02d.%s", cur_time->tm_year + 1900, cur_time->tm_mon + 1, + cur_time->tm_mday, noSpace ? '_' : ' ', cur_time->tm_hour, cur_time->tm_min, cur_time->tm_sec, + extension); + + return string(file); +} + +string GenerateSpecifiedFilename(const char *extension, bool noSpace, const char *format) +{ + BPtr filename = os_generate_formatted_filename(extension, !noSpace, format); + return string(filename); +} + +static void FindBestFilename(string &strPath, bool noSpace) +{ + int num = 2; + + if (!os_file_exists(strPath.c_str())) + return; + + const char *ext = strrchr(strPath.c_str(), '.'); + if (!ext) + return; + + int extStart = int(ext - strPath.c_str()); + for (;;) { + string testPath = strPath; + string numStr; + + numStr = noSpace ? "_" : " ("; + numStr += to_string(num++); + if (!noSpace) + numStr += ")"; + + testPath.insert(extStart, numStr); + + if (!os_file_exists(testPath.c_str())) { + strPath = testPath; + break; + } + } +} + +static void ensure_directory_exists(string &path) +{ + replace(path.begin(), path.end(), '\\', '/'); + + size_t last = path.rfind('/'); + if (last == string::npos) + return; + + string directory = path.substr(0, last); + os_mkdirs(directory.c_str()); +} + +static void remove_reserved_file_characters(string &s) +{ + replace(s.begin(), s.end(), '\\', '/'); + replace(s.begin(), s.end(), '*', '_'); + replace(s.begin(), s.end(), '?', '_'); + replace(s.begin(), s.end(), '"', '_'); + replace(s.begin(), s.end(), '|', '_'); + replace(s.begin(), s.end(), ':', '_'); + replace(s.begin(), s.end(), '>', '_'); + replace(s.begin(), s.end(), '<', '_'); +} + +string GetFormatString(const char *format, const char *prefix, const char *suffix) +{ + string f; + + f = format; + + if (prefix && *prefix) { + string str_prefix = prefix; + + if (str_prefix.back() != ' ') + str_prefix += " "; + + size_t insert_pos = 0; + size_t tmp; + + tmp = f.find_last_of('/'); + if (tmp != string::npos && tmp > insert_pos) + insert_pos = tmp + 1; + + tmp = f.find_last_of('\\'); + if (tmp != string::npos && tmp > insert_pos) + insert_pos = tmp + 1; + + f.insert(insert_pos, str_prefix); + } + + if (suffix && *suffix) { + if (*suffix != ' ') + f += " "; + f += suffix; + } + + remove_reserved_file_characters(f); + + return f; +} + +string GetFormatExt(const char *container) +{ + string ext = container; + if (ext == "fragmented_mp4") + ext = "mp4"; + if (ext == "hybrid_mp4") + ext = "mp4"; + else if (ext == "fragmented_mov") + ext = "mov"; + else if (ext == "hls") + ext = "m3u8"; + else if (ext == "mpegts") + ext = "ts"; + + return ext; +} + +string GetOutputFilename(const char *path, const char *container, bool noSpace, bool overwrite, const char *format) +{ + OBSBasic *main = reinterpret_cast(App()->GetMainWindow()); + + os_dir_t *dir = path && path[0] ? os_opendir(path) : nullptr; + + if (!dir) { + if (main->isVisible()) + OBSMessageBox::warning(main, QTStr("Output.BadPath.Title"), QTStr("Output.BadPath.Text")); + else + main->SysTrayNotify(QTStr("Output.BadPath.Text"), QSystemTrayIcon::Warning); + return ""; + } + + os_closedir(dir); + + string strPath; + strPath += path; + + char lastChar = strPath.back(); + if (lastChar != '/' && lastChar != '\\') + strPath += "/"; + + string ext = GetFormatExt(container); + strPath += GenerateSpecifiedFilename(ext.c_str(), noSpace, format); + ensure_directory_exists(strPath); + if (!overwrite) + FindBestFilename(strPath, noSpace); + + return strPath; +} + +vector> GetLocaleNames() +{ + string path; + if (!GetDataFilePath("locale.ini", path)) + throw "Could not find locale.ini path"; + + ConfigFile ini; + if (ini.Open(path.c_str(), CONFIG_OPEN_EXISTING) != 0) + throw "Could not open locale.ini"; + + size_t sections = config_num_sections(ini); + + vector> names; + names.reserve(sections); + for (size_t i = 0; i < sections; i++) { + const char *tag = config_get_section(ini, i); + const char *name = config_get_string(ini, tag, "Name"); + names.emplace_back(tag, name); + } + + return names; +} + +static void create_log_file(fstream &logFile) +{ + stringstream dst; + + get_last_log(false, "obs-studio/logs", lastLogFile); +#ifdef _WIN32 + get_last_log(true, "obs-studio/crashes", lastCrashLogFile); +#endif + + currentLogFile = GenerateTimeDateFilename("txt"); + dst << "obs-studio/logs/" << currentLogFile.c_str(); + + BPtr path(GetAppConfigPathPtr(dst.str().c_str())); + +#ifdef _WIN32 + BPtr wpath; + os_utf8_to_wcs_ptr(path, 0, &wpath); + logFile.open(wpath, ios_base::in | ios_base::out | ios_base::trunc); +#else + logFile.open(path, ios_base::in | ios_base::out | ios_base::trunc); +#endif + + if (logFile.is_open()) { + delete_oldest_file(false, "obs-studio/logs"); + base_set_log_handler(do_log, &logFile); + } else { + blog(LOG_ERROR, "Failed to open log file"); + } +} + +static auto ProfilerNameStoreRelease = [](profiler_name_store_t *store) { + profiler_name_store_free(store); +}; + +using ProfilerNameStore = std::unique_ptr; + +ProfilerNameStore CreateNameStore() +{ + return ProfilerNameStore{profiler_name_store_create(), ProfilerNameStoreRelease}; +} + +static auto SnapshotRelease = [](profiler_snapshot_t *snap) { + profile_snapshot_free(snap); +}; + +using ProfilerSnapshot = std::unique_ptr; + +ProfilerSnapshot GetSnapshot() +{ + return ProfilerSnapshot{profile_snapshot_create(), SnapshotRelease}; +} + +static void SaveProfilerData(const ProfilerSnapshot &snap) +{ + if (currentLogFile.empty()) + return; + + auto pos = currentLogFile.rfind('.'); + if (pos == currentLogFile.npos) + return; + +#define LITERAL_SIZE(x) x, (sizeof(x) - 1) + ostringstream dst; + dst.write(LITERAL_SIZE("obs-studio/profiler_data/")); + dst.write(currentLogFile.c_str(), pos); + dst.write(LITERAL_SIZE(".csv.gz")); +#undef LITERAL_SIZE + + BPtr path = GetAppConfigPathPtr(dst.str().c_str()); + if (!profiler_snapshot_dump_csv_gz(snap.get(), path)) + blog(LOG_WARNING, "Could not save profiler data to '%s'", static_cast(path)); +} + +static auto ProfilerFree = [](void *) { + profiler_stop(); + + auto snap = GetSnapshot(); + + profiler_print(snap.get()); + profiler_print_time_between_calls(snap.get()); + + SaveProfilerData(snap); + + profiler_free(); +}; + +QAccessibleInterface *accessibleFactory(const QString &classname, QObject *object) +{ + if (classname == QLatin1String("VolumeSlider") && object && object->isWidgetType()) + return new VolumeAccessibleInterface(static_cast(object)); + + return nullptr; +} + +static const char *run_program_init = "run_program_init"; +static int run_program(fstream &logFile, int argc, char *argv[]) +{ + int ret = -1; + + auto profilerNameStore = CreateNameStore(); + + std::unique_ptr prof_release(static_cast(&ProfilerFree), ProfilerFree); + + profiler_start(); + profile_register_root(run_program_init, 0); + + ScopeProfiler prof{run_program_init}; + +#ifdef _WIN32 + QGuiApplication::setHighDpiScaleFactorRoundingPolicy(Qt::HighDpiScaleFactorRoundingPolicy::PassThrough); +#endif + + QCoreApplication::addLibraryPath("."); + +#if __APPLE__ + InstallNSApplicationSubclass(); + InstallNSThreadLocks(); + + if (!isInBundle()) { + blog(LOG_ERROR, + "OBS cannot be run as a standalone binary on macOS. Run the Application bundle instead."); + return ret; + } +#endif + +#if !defined(_WIN32) && !defined(__APPLE__) + /* NOTE: Users blindly set this, but this theme is incompatble with Qt6 and + * crashes loading saved geometry. Just turn off this theme and let users complain OBS + * looks ugly instead of crashing. */ + const char *platform_theme = getenv("QT_QPA_PLATFORMTHEME"); + if (platform_theme && strcmp(platform_theme, "qt5ct") == 0) + unsetenv("QT_QPA_PLATFORMTHEME"); +#endif + + /* NOTE: This disables an optimisation in Qt that attempts to determine if + * any "siblings" intersect with a widget when determining the approximate + * visible/unobscured area. However, by Qt's own admission this is slow + * and in the case of OBS it significantly slows down lists with many + * elements (e.g. Hotkeys) and it is actually faster to disable it. */ + qputenv("QT_NO_SUBTRACTOPAQUESIBLINGS", "1"); + + OBSApp program(argc, argv, profilerNameStore.get()); + try { + QAccessible::installFactory(accessibleFactory); + QFontDatabase::addApplicationFont(":/fonts/OpenSans-Regular.ttf"); + QFontDatabase::addApplicationFont(":/fonts/OpenSans-Bold.ttf"); + QFontDatabase::addApplicationFont(":/fonts/OpenSans-Italic.ttf"); + + bool created_log = false; + + program.AppInit(); + delete_oldest_file(false, "obs-studio/profiler_data"); + + OBSTranslator translator; + program.installTranslator(&translator); + + /* --------------------------------------- */ + /* check and warn if already running */ + + bool cancel_launch = false; + bool already_running = false; + +#ifdef _WIN32 + RunOnceMutex rom = +#endif + CheckIfAlreadyRunning(already_running); + + if (!already_running) { + goto run; + } + + if (!multi) { + QMessageBox mb(QMessageBox::Question, QTStr("AlreadyRunning.Title"), + QTStr("AlreadyRunning.Text")); + mb.addButton(QTStr("AlreadyRunning.LaunchAnyway"), QMessageBox::YesRole); + QPushButton *cancelButton = mb.addButton(QTStr("Cancel"), QMessageBox::NoRole); + mb.setDefaultButton(cancelButton); + + mb.exec(); + cancel_launch = mb.clickedButton() == cancelButton; + } + + if (cancel_launch) + return 0; + + if (!created_log) { + create_log_file(logFile); + created_log = true; + } + + if (multi) { + blog(LOG_INFO, "User enabled --multi flag and is now " + "running multiple instances of OBS."); + } else { + blog(LOG_WARNING, "================================"); + blog(LOG_WARNING, "Warning: OBS is already running!"); + blog(LOG_WARNING, "================================"); + blog(LOG_WARNING, "User is now running multiple " + "instances of OBS!"); + /* Clear unclean_shutdown flag as multiple instances + * running from the same config will lead to a + * false-positive detection.*/ + unclean_shutdown = false; + } + + /* --------------------------------------- */ + run: + +#if !defined(_WIN32) && !defined(__APPLE__) && !defined(__FreeBSD__) + // Mounted by termina during chromeOS linux container startup + // https://chromium.googlesource.com/chromiumos/overlays/board-overlays/+/master/project-termina/chromeos-base/termina-lxd-scripts/files/lxd_setup.sh + os_dir_t *crosDir = os_opendir("/opt/google/cros-containers"); + if (crosDir) { + QMessageBox::StandardButtons buttons(QMessageBox::Ok); + QMessageBox mb(QMessageBox::Critical, QTStr("ChromeOS.Title"), QTStr("ChromeOS.Text"), buttons, + nullptr); + + mb.exec(); + return 0; + } +#endif + + if (!created_log) + create_log_file(logFile); + + if (unclean_shutdown) { + blog(LOG_WARNING, "[Safe Mode] Unclean shutdown detected!"); + } + + if (unclean_shutdown && !safe_mode) { + QMessageBox mb(QMessageBox::Warning, QTStr("AutoSafeMode.Title"), QTStr("AutoSafeMode.Text")); + QPushButton *launchSafeButton = + mb.addButton(QTStr("AutoSafeMode.LaunchSafe"), QMessageBox::AcceptRole); + QPushButton *launchNormalButton = + mb.addButton(QTStr("AutoSafeMode.LaunchNormal"), QMessageBox::RejectRole); + mb.setDefaultButton(launchNormalButton); + mb.exec(); + + safe_mode = mb.clickedButton() == launchSafeButton; + if (safe_mode) { + blog(LOG_INFO, "[Safe Mode] User has launched in Safe Mode."); + } else { + blog(LOG_WARNING, "[Safe Mode] User elected to launch normally."); + } + } + + qInstallMessageHandler([](QtMsgType type, const QMessageLogContext &, const QString &message) { + switch (type) { +#ifdef _DEBUG + case QtDebugMsg: + blog(LOG_DEBUG, "%s", QT_TO_UTF8(message)); + break; + case QtInfoMsg: + blog(LOG_INFO, "%s", QT_TO_UTF8(message)); + break; +#else + case QtDebugMsg: + case QtInfoMsg: + break; +#endif + case QtWarningMsg: + blog(LOG_WARNING, "%s", QT_TO_UTF8(message)); + break; + case QtCriticalMsg: + case QtFatalMsg: + blog(LOG_ERROR, "%s", QT_TO_UTF8(message)); + break; + } + }); + +#ifdef __APPLE__ + MacPermissionStatus audio_permission = CheckPermission(kAudioDeviceAccess); + MacPermissionStatus video_permission = CheckPermission(kVideoDeviceAccess); + MacPermissionStatus accessibility_permission = CheckPermission(kAccessibility); + MacPermissionStatus screen_permission = CheckPermission(kScreenCapture); + + int permissionsDialogLastShown = + config_get_int(App()->GetAppConfig(), "General", "MacOSPermissionsDialogLastShown"); + if (permissionsDialogLastShown < MACOS_PERMISSIONS_DIALOG_VERSION) { + OBSPermissions check(nullptr, screen_permission, video_permission, audio_permission, + accessibility_permission); + check.exec(); + } +#endif + +#ifdef _WIN32 + if (IsRunningOnWine()) { + QMessageBox mb(QMessageBox::Question, QTStr("Wine.Title"), QTStr("Wine.Text")); + mb.setTextFormat(Qt::RichText); + mb.addButton(QTStr("AlreadyRunning.LaunchAnyway"), QMessageBox::AcceptRole); + QPushButton *closeButton = mb.addButton(QMessageBox::Close); + mb.setDefaultButton(closeButton); + + mb.exec(); + if (mb.clickedButton() == closeButton) + return 0; + } +#endif + + if (argc > 1) { + stringstream stor; + stor << argv[1]; + for (int i = 2; i < argc; ++i) { + stor << " " << argv[i]; + } + blog(LOG_INFO, "Command Line Arguments: %s", stor.str().c_str()); + } + + if (!program.OBSInit()) + return 0; + + prof.Stop(); + + ret = program.exec(); + + } catch (const char *error) { + blog(LOG_ERROR, "%s", error); + OBSErrorBox(nullptr, "%s", error); + } + + if (restart || restart_safe) { + arguments = qApp->arguments(); + + if (restart_safe) { + arguments.append("--safe-mode"); + } else { + arguments.removeAll("--safe-mode"); + } + } + + return ret; +} + +#define MAX_CRASH_REPORT_SIZE (150 * 1024) + +#ifdef _WIN32 + +#define CRASH_MESSAGE \ + "Woops, OBS has crashed!\n\nWould you like to copy the crash log " \ + "to the clipboard? The crash log will still be saved to:\n\n%s" + +static void main_crash_handler(const char *format, va_list args, void * /* param */) +{ + char *text = new char[MAX_CRASH_REPORT_SIZE]; + + vsnprintf(text, MAX_CRASH_REPORT_SIZE, format, args); + text[MAX_CRASH_REPORT_SIZE - 1] = 0; + + string crashFilePath = "obs-studio/crashes"; + + delete_oldest_file(true, crashFilePath.c_str()); + + string name = crashFilePath + "/"; + name += "Crash " + GenerateTimeDateFilename("txt"); + + BPtr path(GetAppConfigPathPtr(name.c_str())); + + fstream file; + +#ifdef _WIN32 + BPtr wpath; + os_utf8_to_wcs_ptr(path, 0, &wpath); + file.open(wpath, ios_base::in | ios_base::out | ios_base::trunc | ios_base::binary); +#else + file.open(path, ios_base::in | ios_base::out | ios_base::trunc | ios_base::binary); +#endif + file << text; + file.close(); + + string pathString(path.Get()); + +#ifdef _WIN32 + std::replace(pathString.begin(), pathString.end(), '/', '\\'); +#endif + + string absolutePath = canonical(filesystem::path(pathString)).u8string(); + + size_t size = snprintf(nullptr, 0, CRASH_MESSAGE, absolutePath.c_str()); + + unique_ptr message_buffer(new char[size + 1]); + + snprintf(message_buffer.get(), size + 1, CRASH_MESSAGE, absolutePath.c_str()); + + string finalMessage = string(message_buffer.get(), message_buffer.get() + size); + + int ret = MessageBoxA(NULL, finalMessage.c_str(), "OBS has crashed!", MB_YESNO | MB_ICONERROR | MB_TASKMODAL); + + if (ret == IDYES) { + size_t len = strlen(text); + + HGLOBAL mem = GlobalAlloc(GMEM_MOVEABLE, len); + memcpy(GlobalLock(mem), text, len); + GlobalUnlock(mem); + + OpenClipboard(0); + EmptyClipboard(); + SetClipboardData(CF_TEXT, mem); + CloseClipboard(); + } + + exit(-1); +} + +static void load_debug_privilege(void) +{ + const DWORD flags = TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY; + TOKEN_PRIVILEGES tp; + HANDLE token; + LUID val; + + if (!OpenProcessToken(GetCurrentProcess(), flags, &token)) { + return; + } + + if (!!LookupPrivilegeValue(NULL, SE_DEBUG_NAME, &val)) { + tp.PrivilegeCount = 1; + tp.Privileges[0].Luid = val; + tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED; + + AdjustTokenPrivileges(token, false, &tp, sizeof(tp), NULL, NULL); + } + + if (!!LookupPrivilegeValue(NULL, SE_INC_BASE_PRIORITY_NAME, &val)) { + tp.PrivilegeCount = 1; + tp.Privileges[0].Luid = val; + tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED; + + if (!AdjustTokenPrivileges(token, false, &tp, sizeof(tp), NULL, NULL)) { + blog(LOG_INFO, "Could not set privilege to " + "increase GPU priority"); + } + } + + CloseHandle(token); +} +#endif + +#ifdef __APPLE__ +#define BASE_PATH ".." +#else +#define BASE_PATH "../.." +#endif + +#define CONFIG_PATH BASE_PATH "/config" + +#if defined(ENABLE_PORTABLE_CONFIG) || defined(_WIN32) +#define ALLOW_PORTABLE_MODE 1 +#else +#define ALLOW_PORTABLE_MODE 0 +#endif + +int GetAppConfigPath(char *path, size_t size, const char *name) +{ +#if ALLOW_PORTABLE_MODE + if (portable_mode) { + if (name && *name) { + return snprintf(path, size, CONFIG_PATH "/%s", name); + } else { + return snprintf(path, size, CONFIG_PATH); + } + } else { + return os_get_config_path(path, size, name); + } +#else + return os_get_config_path(path, size, name); +#endif +} + +char *GetAppConfigPathPtr(const char *name) +{ +#if ALLOW_PORTABLE_MODE + if (portable_mode) { + char path[512]; + + if (snprintf(path, sizeof(path), CONFIG_PATH "/%s", name) > 0) { + return bstrdup(path); + } else { + return NULL; + } + } else { + return os_get_config_path_ptr(name); + } +#else + return os_get_config_path_ptr(name); +#endif +} + +int GetProgramDataPath(char *path, size_t size, const char *name) +{ + return os_get_program_data_path(path, size, name); +} + +char *GetProgramDataPathPtr(const char *name) +{ + return os_get_program_data_path_ptr(name); +} + +bool GetFileSafeName(const char *name, std::string &file) +{ + size_t base_len = strlen(name); + size_t len = os_utf8_to_wcs(name, base_len, nullptr, 0); + std::wstring wfile; + + if (!len) + return false; + + wfile.resize(len); + os_utf8_to_wcs(name, base_len, &wfile[0], len + 1); + + for (size_t i = wfile.size(); i > 0; i--) { + size_t im1 = i - 1; + + if (iswspace(wfile[im1])) { + wfile[im1] = '_'; + } else if (wfile[im1] != '_' && !iswalnum(wfile[im1])) { + wfile.erase(im1, 1); + } + } + + if (wfile.size() == 0) + wfile = L"characters_only"; + + len = os_wcs_to_utf8(wfile.c_str(), wfile.size(), nullptr, 0); + if (!len) + return false; + + file.resize(len); + os_wcs_to_utf8(wfile.c_str(), wfile.size(), &file[0], len + 1); + return true; +} + +bool GetClosestUnusedFileName(std::string &path, const char *extension) +{ + size_t len = path.size(); + if (extension) { + path += "."; + path += extension; + } + + if (!os_file_exists(path.c_str())) + return true; + + int index = 1; + + do { + path.resize(len); + path += std::to_string(++index); + if (extension) { + path += "."; + path += extension; + } + } while (os_file_exists(path.c_str())); + + return true; +} + +bool WindowPositionValid(QRect rect) +{ + for (QScreen *screen : QGuiApplication::screens()) { + if (screen->availableGeometry().intersects(rect)) + return true; + } + return false; +} + +static inline bool arg_is(const char *arg, const char *long_form, const char *short_form) +{ + return (long_form && strcmp(arg, long_form) == 0) || (short_form && strcmp(arg, short_form) == 0); +} + +static void check_safe_mode_sentinel(void) +{ +#ifndef NDEBUG + /* Safe Mode detection is disabled in Debug builds to keep developers + * somewhat sane. */ + return; +#else + if (disable_shutdown_check) + return; + + BPtr sentinelPath = GetAppConfigPathPtr("obs-studio/safe_mode"); + if (os_file_exists(sentinelPath)) { + unclean_shutdown = true; + return; + } + + os_quick_write_utf8_file(sentinelPath, nullptr, 0, false); +#endif +} + +static void delete_safe_mode_sentinel(void) +{ +#ifndef NDEBUG + return; +#else + BPtr sentinelPath = GetAppConfigPathPtr("obs-studio/safe_mode"); + os_unlink(sentinelPath); +#endif +} + +#ifndef _WIN32 +void OBSApp::SigIntSignalHandler(int s) +{ + /* Handles SIGINT and writes to a socket. Qt will read + * from the socket in the main thread event loop and trigger + * a call to the ProcessSigInt slot, where we can safely run + * shutdown code without signal safety issues. */ + UNUSED_PARAMETER(s); + + char a = 1; + send(sigintFd[0], &a, sizeof(a), 0); +} +#endif + +void OBSApp::ProcessSigInt(void) +{ + /* This looks weird, but we can't ifdef a Qt slot function so + * the SIGINT handler simply does nothing on Windows. */ +#ifndef _WIN32 + char tmp; + recv(sigintFd[1], &tmp, sizeof(tmp), 0); + + OBSBasic *main = reinterpret_cast(GetMainWindow()); + if (main) + main->close(); +#endif +} + +#ifdef _WIN32 +void OBSApp::commitData(QSessionManager &manager) +{ + if (auto main = App()->GetMainWindow()) { + QMetaObject::invokeMethod(main, "close", Qt::QueuedConnection); + manager.cancel(); + } +} +#endif + +#ifdef _WIN32 +static constexpr char vcRunErrorTitle[] = "Outdated Visual C++ Runtime"; +static constexpr char vcRunErrorMsg[] = "OBS Studio requires a newer version of the Microsoft Visual C++ " + "Redistributables.\n\nYou will now be directed to the download page."; +static constexpr char vcRunInstallerUrl[] = "https://obsproject.com/visual-studio-2022-runtimes"; + +static bool vc_runtime_outdated() +{ + win_version_info ver; + if (!get_dll_ver(L"msvcp140.dll", &ver)) + return true; + /* Major is always 14 (hence 140.dll), so we only care about minor. */ + if (ver.minor >= 40) + return false; + + int choice = MessageBoxA(NULL, vcRunErrorMsg, vcRunErrorTitle, MB_OKCANCEL | MB_ICONERROR | MB_TASKMODAL); + if (choice == IDOK) { + /* Open the URL in the default browser. */ + ShellExecuteA(NULL, "open", vcRunInstallerUrl, NULL, NULL, SW_SHOWNORMAL); + } + + return true; +} +#endif + +int main(int argc, char *argv[]) +{ +#ifndef _WIN32 + signal(SIGPIPE, SIG_IGN); + + struct sigaction sig_handler; + + sig_handler.sa_handler = OBSApp::SigIntSignalHandler; + sigemptyset(&sig_handler.sa_mask); + sig_handler.sa_flags = 0; + + sigaction(SIGINT, &sig_handler, NULL); + + /* Block SIGPIPE in all threads, this can happen if a thread calls write on + a closed pipe. */ + sigset_t sigpipe_mask; + sigemptyset(&sigpipe_mask); + sigaddset(&sigpipe_mask, SIGPIPE); + sigset_t saved_mask; + if (pthread_sigmask(SIG_BLOCK, &sigpipe_mask, &saved_mask) == -1) { + perror("pthread_sigmask"); + exit(1); + } +#endif + +#ifdef _WIN32 + // Abort as early as possible if MSVC runtime is outdated + if (vc_runtime_outdated()) + return 1; + // Try to keep this as early as possible + install_dll_blocklist_hook(); + + obs_init_win32_crash_handler(); + SetErrorMode(SEM_FAILCRITICALERRORS); + load_debug_privilege(); + base_set_crash_handler(main_crash_handler, nullptr); + + const HMODULE hRtwq = LoadLibrary(L"RTWorkQ.dll"); + if (hRtwq) { + typedef HRESULT(STDAPICALLTYPE * PFN_RtwqStartup)(); + PFN_RtwqStartup func = (PFN_RtwqStartup)GetProcAddress(hRtwq, "RtwqStartup"); + func(); + } +#endif + + base_get_log_handler(&def_log_handler, nullptr); + +#if defined(__FreeBSD__) + move_to_xdg(); +#endif + + obs_set_cmdline_args(argc, argv); + + for (int i = 1; i < argc; i++) { + if (arg_is(argv[i], "--multi", "-m")) { + multi = true; + disable_shutdown_check = true; + +#if ALLOW_PORTABLE_MODE + } else if (arg_is(argv[i], "--portable", "-p")) { + portable_mode = true; + +#endif + } else if (arg_is(argv[i], "--verbose", nullptr)) { + log_verbose = true; + + } else if (arg_is(argv[i], "--safe-mode", nullptr)) { + safe_mode = true; + + } else if (arg_is(argv[i], "--only-bundled-plugins", nullptr)) { + disable_3p_plugins = true; + + } else if (arg_is(argv[i], "--disable-shutdown-check", nullptr)) { + /* This exists mostly to bypass the dialog during development. */ + disable_shutdown_check = true; + + } else if (arg_is(argv[i], "--always-on-top", nullptr)) { + opt_always_on_top = true; + + } else if (arg_is(argv[i], "--unfiltered_log", nullptr)) { + unfiltered_log = true; + + } else if (arg_is(argv[i], "--startstreaming", nullptr)) { + opt_start_streaming = true; + + } else if (arg_is(argv[i], "--startrecording", nullptr)) { + opt_start_recording = true; + + } else if (arg_is(argv[i], "--startreplaybuffer", nullptr)) { + opt_start_replaybuffer = true; + + } else if (arg_is(argv[i], "--startvirtualcam", nullptr)) { + opt_start_virtualcam = true; + + } else if (arg_is(argv[i], "--collection", nullptr)) { + if (++i < argc) + opt_starting_collection = argv[i]; + + } else if (arg_is(argv[i], "--profile", nullptr)) { + if (++i < argc) + opt_starting_profile = argv[i]; + + } else if (arg_is(argv[i], "--scene", nullptr)) { + if (++i < argc) + opt_starting_scene = argv[i]; + + } else if (arg_is(argv[i], "--minimize-to-tray", nullptr)) { + opt_minimize_tray = true; + + } else if (arg_is(argv[i], "--studio-mode", nullptr)) { + opt_studio_mode = true; + + } else if (arg_is(argv[i], "--allow-opengl", nullptr)) { + opt_allow_opengl = true; + + } else if (arg_is(argv[i], "--disable-updater", nullptr)) { + opt_disable_updater = true; + + } else if (arg_is(argv[i], "--disable-missing-files-check", nullptr)) { + opt_disable_missing_files_check = true; + + } else if (arg_is(argv[i], "--steam", nullptr)) { + steam = true; + + } else if (arg_is(argv[i], "--help", "-h")) { + std::string help = + "--help, -h: Get list of available commands.\n\n" + "--startstreaming: Automatically start streaming.\n" + "--startrecording: Automatically start recording.\n" + "--startreplaybuffer: Start replay buffer.\n" + "--startvirtualcam: Start virtual camera (if available).\n\n" + "--collection : Use specific scene collection." + "\n" + "--profile : Use specific profile.\n" + "--scene : Start with specific scene.\n\n" + "--studio-mode: Enable studio mode.\n" + "--minimize-to-tray: Minimize to system tray.\n" +#if ALLOW_PORTABLE_MODE + "--portable, -p: Use portable mode.\n" +#endif + "--multi, -m: Don't warn when launching multiple instances.\n\n" + "--safe-mode: Run in Safe Mode (disables third-party plugins, scripting, and WebSockets).\n" + "--only-bundled-plugins: Only load included (first-party) plugins\n" + "--disable-shutdown-check: Disable unclean shutdown detection.\n" + "--verbose: Make log more verbose.\n" + "--always-on-top: Start in 'always on top' mode.\n\n" + "--unfiltered_log: Make log unfiltered.\n\n" + "--disable-updater: Disable built-in updater (Windows/Mac only)\n\n" + "--disable-missing-files-check: Disable the missing files dialog which can appear on startup.\n\n"; + +#ifdef _WIN32 + MessageBoxA(NULL, help.c_str(), "Help", MB_OK | MB_ICONASTERISK); +#else + std::cout << help << "--version, -V: Get current version.\n"; +#endif + exit(0); + + } else if (arg_is(argv[i], "--version", "-V")) { + std::cout << "OBS Studio - " << App()->GetVersionString(false) << "\n"; + exit(0); + } + } + +#if ALLOW_PORTABLE_MODE + if (!portable_mode) { + portable_mode = os_file_exists(BASE_PATH "/portable_mode") || + os_file_exists(BASE_PATH "/obs_portable_mode") || + os_file_exists(BASE_PATH "/portable_mode.txt") || + os_file_exists(BASE_PATH "/obs_portable_mode.txt"); + } + + if (!opt_disable_updater) { + opt_disable_updater = os_file_exists(BASE_PATH "/disable_updater") || + os_file_exists(BASE_PATH "/disable_updater.txt"); + } + + if (!opt_disable_missing_files_check) { + opt_disable_missing_files_check = os_file_exists(BASE_PATH "/disable_missing_files_check") || + os_file_exists(BASE_PATH "/disable_missing_files_check.txt"); + } +#endif + + check_safe_mode_sentinel(); + + fstream logFile; + + curl_global_init(CURL_GLOBAL_ALL); + int ret = run_program(logFile, argc, argv); + +#ifdef _WIN32 + if (hRtwq) { + typedef HRESULT(STDAPICALLTYPE * PFN_RtwqShutdown)(); + PFN_RtwqShutdown func = (PFN_RtwqShutdown)GetProcAddress(hRtwq, "RtwqShutdown"); + func(); + FreeLibrary(hRtwq); + } + + log_blocked_dlls(); +#endif + + delete_safe_mode_sentinel(); + blog(LOG_INFO, "Number of memory leaks: %ld", bnum_allocs()); + base_set_log_handler(nullptr, nullptr); + + if (restart || restart_safe) { + auto executable = arguments.takeFirst(); + QProcess::startDetached(executable, arguments); + } + + return ret; +} diff --git a/frontend/utility/OBSTranslator.hpp b/frontend/utility/OBSTranslator.hpp new file mode 100644 index 000000000..dea5e530b --- /dev/null +++ b/frontend/utility/OBSTranslator.hpp @@ -0,0 +1,280 @@ +/****************************************************************************** + Copyright (C) 2023 by Lain Bailey + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +******************************************************************************/ + +#pragma once + +#include +#include +#include +#include + +#ifndef _WIN32 +#include +#else +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "window-main.hpp" +#include "obs-app-theming.hpp" + +std::string CurrentTimeString(); +std::string CurrentDateTimeString(); +std::string GenerateTimeDateFilename(const char *extension, bool noSpace = false); +std::string GenerateSpecifiedFilename(const char *extension, bool noSpace, const char *format); +std::string GetFormatString(const char *format, const char *prefix, const char *suffix); +std::string GetFormatExt(const char *container); +std::string GetOutputFilename(const char *path, const char *container, bool noSpace, bool overwrite, + const char *format); +QObject *CreateShortcutFilter(); + +struct BaseLexer { + lexer lex; + +public: + inline BaseLexer() { lexer_init(&lex); } + inline ~BaseLexer() { lexer_free(&lex); } + operator lexer *() { return &lex; } +}; + +class OBSTranslator : public QTranslator { + Q_OBJECT + +public: + virtual bool isEmpty() const override { return false; } + + virtual QString translate(const char *context, const char *sourceText, const char *disambiguation, + int n) const override; +}; + +typedef std::function VoidFunc; + +struct UpdateBranch { + QString name; + QString display_name; + QString description; + bool is_enabled; + bool is_visible; +}; + +class OBSApp : public QApplication { + Q_OBJECT + +private: + std::string locale; + + ConfigFile appConfig; + ConfigFile userConfig; + TextLookup textLookup; + QPointer mainWindow; + profiler_name_store_t *profilerNameStore = nullptr; + std::vector updateBranches; + bool branches_loaded = false; + + bool libobs_initialized = false; + + os_inhibit_t *sleepInhibitor = nullptr; + int sleepInhibitRefs = 0; + + bool enableHotkeysInFocus = true; + bool enableHotkeysOutOfFocus = true; + + std::deque translatorHooks; + + bool UpdatePre22MultiviewLayout(const char *layout); + + bool InitGlobalConfig(); + bool InitGlobalConfigDefaults(); + bool InitGlobalLocationDefaults(); + + bool MigrateGlobalSettings(); + void MigrateLegacySettings(uint32_t lastVersion); + + bool InitUserConfig(std::filesystem::path &userConfigLocation, uint32_t lastVersion); + void InitUserConfigDefaults(); + + bool InitLocale(); + bool InitTheme(); + + inline void ResetHotkeyState(bool inFocus); + + QPalette defaultPalette; + OBSTheme *currentTheme = nullptr; + QHash themes; + QPointer themeWatcher; + + void FindThemes(); + + bool notify(QObject *receiver, QEvent *e) override; + +#ifndef _WIN32 + static int sigintFd[2]; + QSocketNotifier *snInt = nullptr; +#else +private slots: + void commitData(QSessionManager &manager); +#endif + +private slots: + void themeFileChanged(const QString &); + +public: + OBSApp(int &argc, char **argv, profiler_name_store_t *store); + ~OBSApp(); + + void AppInit(); + bool OBSInit(); + + void UpdateHotkeyFocusSetting(bool reset = true); + void DisableHotkeys(); + + inline bool HotkeysEnabledInFocus() const { return enableHotkeysInFocus; } + + inline QMainWindow *GetMainWindow() const { return mainWindow.data(); } + + inline config_t *GetAppConfig() const { return appConfig; } + inline config_t *GetUserConfig() const { return userConfig; } + std::filesystem::path userConfigLocation; + std::filesystem::path userScenesLocation; + std::filesystem::path userProfilesLocation; + + inline const char *GetLocale() const { return locale.c_str(); } + + OBSTheme *GetTheme() const { return currentTheme; } + QList GetThemes() const { return themes.values(); } + OBSTheme *GetTheme(const QString &name); + bool SetTheme(const QString &name); + bool IsThemeDark() const { return currentTheme ? currentTheme->isDark : false; } + + void SetBranchData(const std::string &data); + std::vector GetBranches(); + + inline lookup_t *GetTextLookup() const { return textLookup; } + + inline const char *GetString(const char *lookupVal) const { return textLookup.GetString(lookupVal); } + + bool TranslateString(const char *lookupVal, const char **out) const; + + profiler_name_store_t *GetProfilerNameStore() const { return profilerNameStore; } + + const char *GetLastLog() const; + const char *GetCurrentLog() const; + + const char *GetLastCrashLog() const; + + std::string GetVersionString(bool platform = true) const; + bool IsPortableMode(); + bool IsUpdaterDisabled(); + bool IsMissingFilesCheckDisabled(); + + const char *InputAudioSource() const; + const char *OutputAudioSource() const; + + const char *GetRenderModule() const; + + inline void IncrementSleepInhibition() + { + if (!sleepInhibitor) + return; + if (sleepInhibitRefs++ == 0) + os_inhibit_sleep_set_active(sleepInhibitor, true); + } + + inline void DecrementSleepInhibition() + { + if (!sleepInhibitor) + return; + if (sleepInhibitRefs == 0) + return; + if (--sleepInhibitRefs == 0) + os_inhibit_sleep_set_active(sleepInhibitor, false); + } + + inline void PushUITranslation(obs_frontend_translate_ui_cb cb) { translatorHooks.emplace_front(cb); } + + inline void PopUITranslation() { translatorHooks.pop_front(); } +#ifndef _WIN32 + static void SigIntSignalHandler(int); +#endif + +public slots: + void Exec(VoidFunc func); + void ProcessSigInt(); + +signals: + void StyleChanged(); +}; + +int GetAppConfigPath(char *path, size_t size, const char *name); +char *GetAppConfigPathPtr(const char *name); + +int GetProgramDataPath(char *path, size_t size, const char *name); +char *GetProgramDataPathPtr(const char *name); + +inline OBSApp *App() +{ + return static_cast(qApp); +} + +std::vector> GetLocaleNames(); +inline const char *Str(const char *lookup) +{ + return App()->GetString(lookup); +} +inline QString QTStr(const char *lookupVal) +{ + return QString::fromUtf8(Str(lookupVal)); +} + +bool GetFileSafeName(const char *name, std::string &file); +bool GetClosestUnusedFileName(std::string &path, const char *extension); +bool GetUnusedSceneCollectionFile(std::string &name, std::string &file); + +bool WindowPositionValid(QRect rect); + +extern bool portable_mode; +extern bool steam; +extern bool safe_mode; +extern bool disable_3p_plugins; + +extern bool opt_start_streaming; +extern bool opt_start_recording; +extern bool opt_start_replaybuffer; +extern bool opt_start_virtualcam; +extern bool opt_minimize_tray; +extern bool opt_studio_mode; +extern bool opt_allow_opengl; +extern bool opt_always_on_top; +extern std::string opt_starting_scene; +extern bool restart; +extern bool restart_safe; + +#ifdef _WIN32 +extern "C" void install_dll_blocklist_hook(void); +extern "C" void log_blocked_dlls(void); +#endif