mirror of
https://github.com/obsproject/obs-studio.git
synced 2026-01-27 23:59:43 -05:00
Windows and Linux allow the storage of configuration files relative to the binary location, which is enabled by default on Windows and has to be explicitly enabled on Linux. This was originally conflated with the LINUX_PORTABLE build setting which also allowed the application itself to be run from non-default locations on a Linux system. This change reintroduces the functionality behind the ENABLE_PORTABLE_CONFIG build setting on Linux. It also adds necessary code to make this setting compatible with the recently introduced relocatable settings code changes: When portable mode is enabled, user configuration, scene collections, and profiles are stored in the config directory created for portable mode.
2892 lines
71 KiB
C++
2892 lines
71 KiB
C++
/******************************************************************************
|
|
Copyright (C) 2023 by Lain Bailey <lain@obsproject.com>
|
|
|
|
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 <http://www.gnu.org/licenses/>.
|
|
******************************************************************************/
|
|
|
|
#include <time.h>
|
|
#include <stdio.h>
|
|
#include <wchar.h>
|
|
#include <chrono>
|
|
#include <ratio>
|
|
#include <string>
|
|
#include <sstream>
|
|
#include <mutex>
|
|
#include <filesystem>
|
|
#include <util/bmem.h>
|
|
#include <util/dstr.hpp>
|
|
#include <util/platform.h>
|
|
#include <util/profiler.hpp>
|
|
#include <util/cf-parser.h>
|
|
#include <obs-config.h>
|
|
#include <obs.hpp>
|
|
#include <qt-wrappers.hpp>
|
|
#include <slider-ignorewheel.hpp>
|
|
|
|
#include <QDir>
|
|
#include <QFile>
|
|
#include <QGuiApplication>
|
|
#include <QScreen>
|
|
#include <QProcess>
|
|
#include <QAccessible>
|
|
|
|
#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 <fstream>
|
|
|
|
#include <curl/curl.h>
|
|
|
|
#ifdef _WIN32
|
|
#include <windows.h>
|
|
#include <filesystem>
|
|
#include <util/windows/win-version.h>
|
|
#else
|
|
#include <signal.h>
|
|
#include <pthread.h>
|
|
#include <sys/types.h>
|
|
#include <sys/socket.h>
|
|
#include <unistd.h>
|
|
#endif
|
|
|
|
#if defined(_WIN32) || defined(ENABLE_SPARKLE_UPDATER)
|
|
#include "update/models/branches.hpp"
|
|
#endif
|
|
|
|
#if !defined(_WIN32) && !defined(__APPLE__)
|
|
#include <obs-nix-platform.h>
|
|
#include <qpa/qplatformnativeinterface.h>
|
|
#endif
|
|
|
|
#include <iostream>
|
|
|
|
#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> 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<QDialog *>(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<QMouseEvent *>(event));
|
|
|
|
/*case QEvent::MouseButtonDblClick:
|
|
case QEvent::Wheel:*/
|
|
case QEvent::KeyPress:
|
|
case QEvent::KeyRelease:
|
|
return key_event(static_cast<QKeyEvent *>(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<system_clock::period, seconds::period>::value &&
|
|
written && (sizeof(buf) - written) > 5) {
|
|
auto tp_secs = time_point_cast<seconds>(tp);
|
|
auto millis = duration_cast<milliseconds>(tp - tp_secs).count();
|
|
|
|
snprintf(buf + written, sizeof(buf) - written, ".%03u",
|
|
static_cast<unsigned>(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<mutex> 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<fstream *>(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<mutex> 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<int>(
|
|
MultiviewLayout::HORIZONTAL_TOP_8_SCENES));
|
|
return true;
|
|
}
|
|
|
|
if (astrcmpi(layout, "horizontalbottom") == 0) {
|
|
config_set_int(
|
|
userConfig, "BasicWindow", "MultiviewLayout",
|
|
static_cast<int>(
|
|
MultiviewLayout::HORIZONTAL_BOTTOM_8_SCENES));
|
|
return true;
|
|
}
|
|
|
|
if (astrcmpi(layout, "verticalleft") == 0) {
|
|
config_set_int(
|
|
userConfig, "BasicWindow", "MultiviewLayout",
|
|
static_cast<int>(
|
|
MultiviewLayout::VERTICAL_LEFT_8_SCENES));
|
|
return true;
|
|
}
|
|
|
|
if (astrcmpi(layout, "verticalright") == 0) {
|
|
config_set_int(
|
|
userConfig, "BasicWindow", "MultiviewLayout",
|
|
static_cast<int>(
|
|
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)
|
|
{
|
|
bool hasChanges = false;
|
|
|
|
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;
|
|
}
|
|
|
|
hasChanges = MigrateLegacySettings(lastVersion);
|
|
|
|
if (!opt_starting_collection.empty()) {
|
|
const OBSBasic *basic =
|
|
reinterpret_cast<OBSBasic *>(GetMainWindow());
|
|
const std::optional<OBSSceneCollection> foundCollection =
|
|
basic->GetSceneCollectionByName(
|
|
opt_starting_collection);
|
|
|
|
if (foundCollection) {
|
|
config_set_string(userConfig, "Basic",
|
|
"SceneCollection",
|
|
foundCollection.value().name.c_str());
|
|
config_set_string(
|
|
userConfig, "Basic", "SceneCollectionFile",
|
|
foundCollection.value().fileName.c_str());
|
|
hasChanges = true;
|
|
}
|
|
}
|
|
|
|
if (!opt_starting_profile.empty()) {
|
|
const OBSBasic *basic =
|
|
reinterpret_cast<OBSBasic *>(GetMainWindow());
|
|
|
|
const std::optional<OBSProfile> foundProfile =
|
|
basic->GetProfileByName(opt_starting_profile);
|
|
|
|
if (foundProfile) {
|
|
config_set_string(userConfig, "Basic", "Profile",
|
|
foundProfile.value().name.c_str());
|
|
config_set_string(
|
|
userConfig, "Basic", "ProfileDir",
|
|
foundProfile.value().directoryName.c_str());
|
|
|
|
hasChanges = true;
|
|
}
|
|
}
|
|
|
|
if (hasChanges) {
|
|
config_save_safe(userConfig, "tmp", nullptr);
|
|
}
|
|
|
|
InitUserConfigDefaults();
|
|
|
|
return true;
|
|
}
|
|
|
|
bool 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<uint32_t, string> 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;
|
|
}
|
|
|
|
return hasChanges;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
std::filesystem::copy(legacyGlobalConfigFile, userConfigFile);
|
|
|
|
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<UpdateBranch> &out,
|
|
std::string &error)
|
|
{
|
|
JsonBranches branches;
|
|
|
|
try {
|
|
nlohmann::json json = nlohmann::json::parse(jsonString);
|
|
branches = json.get<JsonBranches>();
|
|
} 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<UpdateBranch> &out)
|
|
{
|
|
string error;
|
|
string branchesText;
|
|
|
|
BPtr<char> 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<UpdateBranch> 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<UpdateBranch> OBSApp::GetBranches()
|
|
{
|
|
vector<UpdateBranch> 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<UpdateBranch> 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>("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(userConfig, "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<QWidget *>(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<OBSBasic *>(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());
|
|
}
|
|
|
|
static void delete_oldest_file(bool has_prefix, const char *location)
|
|
{
|
|
BPtr<char> 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()->GetUserConfig(), "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<char> 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<char> 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<OBSBasic *>(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<pair<string, string>> 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<pair<string, string>> 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<char> path(GetAppConfigPathPtr(dst.str().c_str()));
|
|
|
|
#ifdef _WIN32
|
|
BPtr<wchar_t> 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<profiler_name_store_t,
|
|
decltype(ProfilerNameStoreRelease)>;
|
|
|
|
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<profiler_snapshot_t, decltype(SnapshotRelease)>;
|
|
|
|
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<char> 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<const char *>(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<QWidget *>(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<void, decltype(ProfilerFree)> prof_release(
|
|
static_cast<void *>(&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<char> path(GetAppConfigPathPtr(name.c_str()));
|
|
|
|
fstream file;
|
|
|
|
#ifdef _WIN32
|
|
BPtr<wchar_t> 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<char[]> 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<OBSBasic *>(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);
|
|
|
|
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 <string>: Use specific scene collection."
|
|
"\n"
|
|
"--profile <string>: Use specific profile.\n"
|
|
"--scene <string>: 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;
|
|
}
|