mirror of
https://github.com/obsproject/obs-studio.git
synced 2025-12-27 23:45:48 +00:00
frontend: Provide UI for phase 1 of plugin manager
For phase 1 of the plugin manager, the ability to toggle off/on plugins to be loaded at launch is provided. This commit adds a new Plugin Manager dialog which can be accessed from the Tools menu, which shows a list of all installed 3rd party plugins with a checkbox to toggle them off or on. If a change is made, the user is prompted to restart OBS. To allow this, the plugin manager uses a json based config file stored in the OBS config directory. Additionally for sources in the source tree, a sample UI has been provided that indicates any sources whose parent module is disabled, by turning its title red.
This commit is contained in:
parent
14004cec96
commit
23b67268e7
@ -46,6 +46,7 @@ include(cmake/ui-components.cmake)
|
||||
include(cmake/ui-dialogs.cmake)
|
||||
include(cmake/ui-docks.cmake)
|
||||
include(cmake/feature-importers.cmake)
|
||||
include(cmake/feature-plugin-manager.cmake)
|
||||
include(cmake/ui-models.cmake)
|
||||
include(cmake/ui-oauth.cmake)
|
||||
include(cmake/feature-browserpanels.cmake)
|
||||
|
||||
@ -19,6 +19,7 @@
|
||||
|
||||
#include <components/Multiview.hpp>
|
||||
#include <dialogs/LogUploadDialog.hpp>
|
||||
#include <plugin-manager/PluginManager.hpp>
|
||||
#include <utility/CrashHandler.hpp>
|
||||
#include <utility/OBSEventFilter.hpp>
|
||||
#include <utility/OBSProxyStyle.hpp>
|
||||
@ -323,6 +324,7 @@ bool OBSApp::InitGlobalLocationDefaults()
|
||||
config_set_default_string(appConfig, "Locations", "Configuration", path);
|
||||
config_set_default_string(appConfig, "Locations", "SceneCollections", path);
|
||||
config_set_default_string(appConfig, "Locations", "Profiles", path);
|
||||
config_set_default_string(appConfig, "Locations", "PluginManagerSettings", path);
|
||||
|
||||
return true;
|
||||
}
|
||||
@ -422,6 +424,7 @@ static bool MakeUserDirs()
|
||||
|
||||
constexpr std::string_view OBSProfileSubDirectory = "obs-studio/basic/profiles";
|
||||
constexpr std::string_view OBSScenesSubDirectory = "obs-studio/basic/scenes";
|
||||
constexpr std::string_view OBSPluginManagerSubDirectory = "obs-studio/plugin_manager";
|
||||
|
||||
static bool MakeUserProfileDirs()
|
||||
{
|
||||
@ -429,6 +432,8 @@ static bool MakeUserProfileDirs()
|
||||
App()->userProfilesLocation / std::filesystem::u8path(OBSProfileSubDirectory);
|
||||
const std::filesystem::path userScenesPath =
|
||||
App()->userScenesLocation / std::filesystem::u8path(OBSScenesSubDirectory);
|
||||
const std::filesystem::path userPluginManagerPath =
|
||||
App()->userPluginManagerSettingsLocation / std::filesystem::u8path(OBSPluginManagerSubDirectory);
|
||||
|
||||
if (!std::filesystem::exists(userProfilePath)) {
|
||||
try {
|
||||
@ -450,6 +455,16 @@ static bool MakeUserProfileDirs()
|
||||
}
|
||||
}
|
||||
|
||||
if (!std::filesystem::exists(userPluginManagerPath)) {
|
||||
try {
|
||||
std::filesystem::create_directories(userPluginManagerPath);
|
||||
} catch (const std::filesystem::filesystem_error &error) {
|
||||
blog(LOG_ERROR, "Failed to create user plugin manager directory '%s'\n%s",
|
||||
userPluginManagerPath.u8string().c_str(), error.what());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -522,11 +537,14 @@ bool OBSApp::InitGlobalConfig()
|
||||
std::filesystem::u8path(config_get_default_string(appConfig, "Locations", "SceneCollections"));
|
||||
std::filesystem::path defaultUserProfilesLocation =
|
||||
std::filesystem::u8path(config_get_default_string(appConfig, "Locations", "Profiles"));
|
||||
std::filesystem::path defaultPluginManagerLocation =
|
||||
std::filesystem::u8path(config_get_default_string(appConfig, "Locations", "PluginManagerSettings"));
|
||||
|
||||
if (IsPortableMode()) {
|
||||
userConfigLocation = std::move(defaultUserConfigLocation);
|
||||
userScenesLocation = std::move(defaultUserScenesLocation);
|
||||
userProfilesLocation = std::move(defaultUserProfilesLocation);
|
||||
userPluginManagerSettingsLocation = std::move(defaultPluginManagerLocation);
|
||||
} else {
|
||||
std::filesystem::path currentUserConfigLocation =
|
||||
std::filesystem::u8path(config_get_string(appConfig, "Locations", "Configuration"));
|
||||
@ -534,6 +552,8 @@ bool OBSApp::InitGlobalConfig()
|
||||
std::filesystem::u8path(config_get_string(appConfig, "Locations", "SceneCollections"));
|
||||
std::filesystem::path currentUserProfilesLocation =
|
||||
std::filesystem::u8path(config_get_string(appConfig, "Locations", "Profiles"));
|
||||
std::filesystem::path currentUserPluginManagerLocation =
|
||||
std::filesystem::u8path(config_get_string(appConfig, "Locations", "PluginManagerSettings"));
|
||||
|
||||
userConfigLocation = (std::filesystem::exists(currentUserConfigLocation))
|
||||
? std::move(currentUserConfigLocation)
|
||||
@ -544,6 +564,9 @@ bool OBSApp::InitGlobalConfig()
|
||||
userProfilesLocation = (std::filesystem::exists(currentUserProfilesLocation))
|
||||
? std::move(currentUserProfilesLocation)
|
||||
: std::move(defaultUserProfilesLocation);
|
||||
userPluginManagerSettingsLocation = (std::filesystem::exists(currentUserPluginManagerLocation))
|
||||
? std::move(currentUserPluginManagerLocation)
|
||||
: std::move(defaultPluginManagerLocation);
|
||||
}
|
||||
|
||||
bool userConfigResult = InitUserConfig(userConfigLocation, lastVersion);
|
||||
@ -867,6 +890,7 @@ OBSApp::OBSApp(int &argc, char **argv, profiler_name_store_t *store)
|
||||
#endif
|
||||
|
||||
setDesktopFileName("com.obsproject.Studio");
|
||||
pluginManager_ = std::make_unique<OBS::PluginManager>();
|
||||
}
|
||||
|
||||
OBSApp::~OBSApp()
|
||||
@ -1769,3 +1793,20 @@ void OBSApp::addLogLine(int logLevel, const QString &message)
|
||||
{
|
||||
emit logLineAdded(logLevel, message);
|
||||
}
|
||||
|
||||
void OBSApp::loadAppModules(struct obs_module_failure_info &mfi)
|
||||
{
|
||||
pluginManager_->preLoad();
|
||||
blog(LOG_INFO, "---------------------------------");
|
||||
obs_load_all_modules2(&mfi);
|
||||
blog(LOG_INFO, "---------------------------------");
|
||||
obs_log_loaded_modules();
|
||||
blog(LOG_INFO, "---------------------------------");
|
||||
obs_post_load_modules();
|
||||
pluginManager_->postLoad();
|
||||
}
|
||||
|
||||
void OBSApp::pluginManagerOpenDialog()
|
||||
{
|
||||
pluginManager_->open();
|
||||
}
|
||||
|
||||
@ -47,6 +47,7 @@ class CrashHandler;
|
||||
|
||||
enum class LogFileType { NoType, CurrentAppLog, LastAppLog, CrashLog };
|
||||
enum class LogFileState { NoState, New, Uploaded };
|
||||
class PluginManager;
|
||||
} // namespace OBS
|
||||
|
||||
struct UpdateBranch {
|
||||
@ -84,6 +85,8 @@ private:
|
||||
|
||||
std::deque<obs_frontend_translate_ui_cb> translatorHooks;
|
||||
|
||||
std::unique_ptr<OBS::PluginManager> pluginManager_;
|
||||
|
||||
bool UpdatePre22MultiviewLayout(const char *layout);
|
||||
|
||||
bool InitGlobalConfig();
|
||||
@ -144,6 +147,7 @@ public:
|
||||
std::filesystem::path userConfigLocation;
|
||||
std::filesystem::path userScenesLocation;
|
||||
std::filesystem::path userProfilesLocation;
|
||||
std::filesystem::path userPluginManagerSettingsLocation;
|
||||
|
||||
inline const char *GetLocale() const { return locale.c_str(); }
|
||||
|
||||
@ -210,6 +214,11 @@ public:
|
||||
static void SigIntSignalHandler(int);
|
||||
#endif
|
||||
|
||||
void loadAppModules(struct obs_module_failure_info &mfi);
|
||||
|
||||
// Plugin Manager Accessors
|
||||
void pluginManagerOpenDialog();
|
||||
|
||||
public slots:
|
||||
void Exec(VoidFunc func);
|
||||
void ProcessSigInt();
|
||||
|
||||
8
frontend/cmake/feature-plugin-manager.cmake
Normal file
8
frontend/cmake/feature-plugin-manager.cmake
Normal file
@ -0,0 +1,8 @@
|
||||
target_sources(
|
||||
obs-studio
|
||||
PRIVATE
|
||||
plugin-manager/PluginManager.cpp
|
||||
plugin-manager/PluginManager.hpp
|
||||
plugin-manager/PluginManagerWindow.cpp
|
||||
plugin-manager/PluginManagerWindow.hpp
|
||||
)
|
||||
@ -47,6 +47,7 @@ target_sources(
|
||||
forms/OBSRemux.ui
|
||||
forms/StatusBarWidget.ui
|
||||
forms/obs.qrc
|
||||
forms/PluginManagerWindow.ui
|
||||
forms/source-toolbar/browser-source-toolbar.ui
|
||||
forms/source-toolbar/color-source-toolbar.ui
|
||||
forms/source-toolbar/device-select-toolbar.ui
|
||||
|
||||
@ -9,6 +9,8 @@
|
||||
#include <QLineEdit>
|
||||
#include <QPainter>
|
||||
|
||||
#include "plugin-manager/PluginManager.hpp"
|
||||
|
||||
#include "moc_SourceTreeItem.cpp"
|
||||
|
||||
static inline OBSScene GetCurrentScene()
|
||||
@ -84,6 +86,16 @@ SourceTreeItem::SourceTreeItem(SourceTree *tree_, OBSSceneItem sceneitem_) : tre
|
||||
label->setAttribute(Qt::WA_TranslucentBackground);
|
||||
label->setEnabled(sourceVisible);
|
||||
|
||||
const char *sourceId = obs_source_get_unversioned_id(source);
|
||||
switch (obs_source_load_state(sourceId)) {
|
||||
case OBS_MODULE_DISABLED:
|
||||
case OBS_MODULE_MISSING:
|
||||
label->setStyleSheet("QLabel {color: #CC0000;}");
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
#ifdef __APPLE__
|
||||
vis->setAttribute(Qt::WA_LayoutUsesWidgetRect);
|
||||
lock->setAttribute(Qt::WA_LayoutUsesWidgetRect);
|
||||
|
||||
@ -1621,3 +1621,11 @@ MultitrackVideo.IncompatibleSettings.UpdateAndStartStreaming="Update Settings an
|
||||
MultitrackVideo.IncompatibleSettings.AudioChannels="%1 is not currently compatible with [Audio → General → Channels] set to '%2', %3"
|
||||
MultitrackVideo.IncompatibleSettings.AudioChannelsSingle="[Audio → General → Channels] needs to be set to '%1'"
|
||||
MultitrackVideo.IncompatibleSettings.AudioChannelsMultiple="%1 requires multiple different settings for [Audio → General → Channels]"
|
||||
|
||||
#Plugin ManagerW
|
||||
Basic.OpenPluginManager="Plugin Manager"
|
||||
PluginManager="Plugin Manager"
|
||||
PluginManager.HelpText="Plugin Manager"
|
||||
PluginManager.Restart="Restart OBS?"
|
||||
PluginManager.NeedsRestart="To apply these changes, OBS needs to restart. Do you want to restart now?"
|
||||
PluginManager.MissingPlugin="[PLUGIN NOT FOUND]"
|
||||
|
||||
@ -938,6 +938,7 @@
|
||||
</property>
|
||||
<addaction name="autoConfigure"/>
|
||||
<addaction name="idianPlayground"/>
|
||||
<addaction name="actionOpenPluginManager"/>
|
||||
<addaction name="separator"/>
|
||||
</widget>
|
||||
<widget class="QMenu" name="menuDocks">
|
||||
@ -2181,6 +2182,11 @@
|
||||
<string>Basic.AutoConfig</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionOpenPluginManager">
|
||||
<property name="text">
|
||||
<string>Basic.OpenPluginManager</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="stats">
|
||||
<property name="text">
|
||||
<string>Basic.Stats</string>
|
||||
|
||||
48
frontend/forms/PluginManagerWindow.ui
Normal file
48
frontend/forms/PluginManagerWindow.ui
Normal file
@ -0,0 +1,48 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>PluginManagerWindow</class>
|
||||
<widget class="QDialog" name="PluginManagerWindow">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>850</width>
|
||||
<height>400</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>PluginManager</string>
|
||||
</property>
|
||||
<property name="sizeGripEnabled">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<item row="2" column="0">
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_4">
|
||||
<property name="spacing">
|
||||
<number>6</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QDialogButtonBox" name="buttonBox">
|
||||
<property name="standardButtons">
|
||||
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="text">
|
||||
<string>PluginManager.HelpText</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QListWidget" name="modulesList"/>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
||||
303
frontend/plugin-manager/PluginManager.cpp
Normal file
303
frontend/plugin-manager/PluginManager.cpp
Normal file
@ -0,0 +1,303 @@
|
||||
/******************************************************************************
|
||||
Copyright (C) 2025 by FiniteSingularity <finitesingularityttv@gmail.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 "PluginManager.hpp"
|
||||
#include "PluginManagerWindow.hpp"
|
||||
|
||||
#include <OBSApp.hpp>
|
||||
#include <qt-wrappers.hpp>
|
||||
#include <widgets/OBSBasic.hpp>
|
||||
|
||||
#include <QMessageBox>
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <fstream>
|
||||
|
||||
extern bool restart;
|
||||
|
||||
namespace OBS {
|
||||
|
||||
void addModuleToPluginManagerImpl(void *param, obs_module_t *newModule)
|
||||
{
|
||||
auto &instance = *static_cast<OBS::PluginManager *>(param);
|
||||
std::string moduleName = obs_get_module_file_name(newModule);
|
||||
moduleName = moduleName.substr(0, moduleName.rfind("."));
|
||||
|
||||
if (!obs_get_module_allow_disable(moduleName.c_str()))
|
||||
return;
|
||||
|
||||
const char *display_name = obs_get_module_name(newModule);
|
||||
std::string module_name = moduleName;
|
||||
const char *id = obs_get_module_id(newModule);
|
||||
const char *version = obs_get_module_version(newModule);
|
||||
|
||||
auto it = std::find_if(instance.modules_.begin(), instance.modules_.end(),
|
||||
[&](OBS::ModuleInfo module) { return module.module_name == moduleName; });
|
||||
|
||||
if (it == instance.modules_.end()) {
|
||||
instance.modules_.push_back({display_name ? display_name : "", module_name, id ? id : "",
|
||||
version ? version : "", true, true});
|
||||
} else {
|
||||
it->display_name = display_name ? display_name : "";
|
||||
it->module_name = module_name;
|
||||
it->id = id ? id : "";
|
||||
it->version = version ? version : "";
|
||||
}
|
||||
}
|
||||
|
||||
constexpr std::string_view OBSPluginManagerPath = "obs-studio/plugin_manager";
|
||||
constexpr std::string_view OBSPluginManagerModulesFile = "modules.json";
|
||||
|
||||
void PluginManager::preLoad()
|
||||
{
|
||||
loadModules_();
|
||||
disableModules_();
|
||||
}
|
||||
|
||||
void PluginManager::postLoad()
|
||||
{
|
||||
// Find any new modules and add to Plugin Manager.
|
||||
obs_enum_modules(addModuleToPluginManager, this);
|
||||
// Get list of valid module types.
|
||||
addModuleTypes_();
|
||||
saveModules_();
|
||||
// Add provided features from any unloaded modules
|
||||
linkUnloadedModules_();
|
||||
}
|
||||
|
||||
std::filesystem::path PluginManager::getConfigFilePath_()
|
||||
{
|
||||
std::filesystem::path path = App()->userPluginManagerSettingsLocation /
|
||||
std::filesystem::u8path(OBSPluginManagerPath) /
|
||||
std::filesystem::u8path(OBSPluginManagerModulesFile);
|
||||
return path;
|
||||
}
|
||||
|
||||
void PluginManager::loadModules_()
|
||||
{
|
||||
auto modulesFile = getConfigFilePath_();
|
||||
if (std::filesystem::exists(modulesFile)) {
|
||||
std::ifstream jsonFile(modulesFile);
|
||||
nlohmann::json data = nlohmann::json::parse(jsonFile);
|
||||
modules_.clear();
|
||||
for (auto it : data) {
|
||||
ModuleInfo obsModule;
|
||||
try {
|
||||
obsModule = {it.at("display_name"),
|
||||
it.at("module_name"),
|
||||
it.at("id"),
|
||||
it.at("version"),
|
||||
it.at("enabled"),
|
||||
it.at("enabled"),
|
||||
it.at("sources"),
|
||||
it.at("outputs"),
|
||||
it.at("encoders"),
|
||||
it.at("services"),
|
||||
{},
|
||||
{},
|
||||
{},
|
||||
{}};
|
||||
} catch (const nlohmann::json::out_of_range &error) {
|
||||
blog(LOG_WARNING, "Error loading module info: %s", error.what());
|
||||
continue;
|
||||
}
|
||||
modules_.push_back(obsModule);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void PluginManager::linkUnloadedModules_()
|
||||
{
|
||||
for (const auto &moduleInfo : modules_) {
|
||||
if (!moduleInfo.enabled) {
|
||||
auto obsModule = obs_get_disabled_module(moduleInfo.module_name.c_str());
|
||||
if (!obsModule) {
|
||||
continue;
|
||||
}
|
||||
for (const auto &source : moduleInfo.sources) {
|
||||
obs_module_add_source(obsModule, source.c_str());
|
||||
}
|
||||
for (const auto &output : moduleInfo.outputs) {
|
||||
obs_module_add_output(obsModule, output.c_str());
|
||||
}
|
||||
for (const auto &encoder : moduleInfo.encoders) {
|
||||
obs_module_add_encoder(obsModule, encoder.c_str());
|
||||
}
|
||||
for (const auto &service : moduleInfo.services) {
|
||||
obs_module_add_service(obsModule, service.c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void PluginManager::saveModules_()
|
||||
{
|
||||
auto modulesFile = getConfigFilePath_();
|
||||
std::ofstream outFile(modulesFile);
|
||||
nlohmann::json data = nlohmann::json::array();
|
||||
|
||||
for (auto const &moduleInfo : modules_) {
|
||||
nlohmann::json modData;
|
||||
modData["display_name"] = moduleInfo.display_name;
|
||||
modData["module_name"] = moduleInfo.module_name;
|
||||
modData["id"] = moduleInfo.id;
|
||||
modData["version"] = moduleInfo.version;
|
||||
modData["enabled"] = moduleInfo.enabled;
|
||||
modData["sources"] = moduleInfo.sources;
|
||||
modData["outputs"] = moduleInfo.outputs;
|
||||
modData["encoders"] = moduleInfo.encoders;
|
||||
modData["services"] = moduleInfo.services;
|
||||
data.push_back(modData);
|
||||
}
|
||||
outFile << std::setw(4) << data << std::endl;
|
||||
}
|
||||
|
||||
void PluginManager::addModuleTypes_()
|
||||
{
|
||||
const char *source_id;
|
||||
int i = 0;
|
||||
while (obs_enum_source_types(i, &source_id)) {
|
||||
i += 1;
|
||||
obs_module_t *obsModule = obs_source_get_module(source_id);
|
||||
if (!obsModule) {
|
||||
continue;
|
||||
}
|
||||
std::string moduleName = obs_get_module_file_name(obsModule);
|
||||
moduleName = moduleName.substr(0, moduleName.rfind("."));
|
||||
auto it = std::find_if(modules_.begin(), modules_.end(),
|
||||
[moduleName](ModuleInfo const &m) { return m.module_name == moduleName; });
|
||||
if (it != modules_.end()) {
|
||||
it->sourcesLoaded.push_back(source_id);
|
||||
}
|
||||
}
|
||||
|
||||
const char *output_id;
|
||||
i = 0;
|
||||
while (obs_enum_output_types(i, &output_id)) {
|
||||
i += 1;
|
||||
obs_module_t *obsModule = obs_source_get_module(output_id);
|
||||
if (!obsModule) {
|
||||
continue;
|
||||
}
|
||||
std::string moduleName = obs_get_module_file_name(obsModule);
|
||||
moduleName = moduleName.substr(0, moduleName.rfind("."));
|
||||
auto it = std::find_if(modules_.begin(), modules_.end(),
|
||||
[moduleName](ModuleInfo const &m) { return m.module_name == moduleName; });
|
||||
if (it != modules_.end()) {
|
||||
it->outputsLoaded.push_back(output_id);
|
||||
}
|
||||
}
|
||||
|
||||
const char *encoder_id;
|
||||
i = 0;
|
||||
while (obs_enum_encoder_types(i, &encoder_id)) {
|
||||
i += 1;
|
||||
obs_module_t *obsModule = obs_source_get_module(encoder_id);
|
||||
if (!obsModule) {
|
||||
continue;
|
||||
}
|
||||
std::string moduleName = obs_get_module_file_name(obsModule);
|
||||
moduleName = moduleName.substr(0, moduleName.rfind("."));
|
||||
auto it = std::find_if(modules_.begin(), modules_.end(),
|
||||
[moduleName](ModuleInfo const &m) { return m.module_name == moduleName; });
|
||||
if (it != modules_.end()) {
|
||||
it->encodersLoaded.push_back(encoder_id);
|
||||
}
|
||||
}
|
||||
|
||||
const char *service_id;
|
||||
i = 0;
|
||||
while (obs_enum_service_types(i, &service_id)) {
|
||||
i += 1;
|
||||
obs_module_t *obsModule = obs_source_get_module(service_id);
|
||||
if (!obsModule) {
|
||||
continue;
|
||||
}
|
||||
std::string moduleName = obs_get_module_file_name(obsModule);
|
||||
moduleName = moduleName.substr(0, moduleName.rfind("."));
|
||||
auto it = std::find_if(modules_.begin(), modules_.end(),
|
||||
[moduleName](ModuleInfo const &m) { return m.module_name == moduleName; });
|
||||
if (it != modules_.end()) {
|
||||
it->servicesLoaded.push_back(service_id);
|
||||
}
|
||||
}
|
||||
|
||||
for (auto &moduleInfo : modules_) {
|
||||
if (moduleInfo.enabledAtLaunch) {
|
||||
moduleInfo.sources = moduleInfo.sourcesLoaded;
|
||||
moduleInfo.encoders = moduleInfo.encodersLoaded;
|
||||
moduleInfo.outputs = moduleInfo.outputsLoaded;
|
||||
moduleInfo.services = moduleInfo.servicesLoaded;
|
||||
} else {
|
||||
for (auto const &source : moduleInfo.sources) {
|
||||
disabledSources_.push_back(source);
|
||||
}
|
||||
for (auto const &output : moduleInfo.outputs) {
|
||||
disabledOutputs_.push_back(output);
|
||||
}
|
||||
for (auto const &encoder : moduleInfo.encoders) {
|
||||
disabledEncoders_.push_back(encoder);
|
||||
}
|
||||
for (auto const &service : moduleInfo.services) {
|
||||
disabledServices_.push_back(service);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void PluginManager::disableModules_()
|
||||
{
|
||||
for (const auto &moduleInfo : modules_) {
|
||||
if (!moduleInfo.enabled) {
|
||||
obs_add_disabled_module(moduleInfo.module_name.c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void PluginManager::open()
|
||||
{
|
||||
auto main = OBSBasic::Get();
|
||||
PluginManagerWindow pluginManagerWindow(modules_, main);
|
||||
auto result = pluginManagerWindow.exec();
|
||||
if (result == QDialog::Accepted) {
|
||||
modules_ = pluginManagerWindow.result();
|
||||
saveModules_();
|
||||
|
||||
bool changed = false;
|
||||
|
||||
for (auto const &moduleInfo : modules_) {
|
||||
if (moduleInfo.enabled != moduleInfo.enabledAtLaunch) {
|
||||
changed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (changed) {
|
||||
QMessageBox::StandardButton button = OBSMessageBox::question(
|
||||
main, QTStr("PluginManager.Restart"), QTStr("PluginManager.NeedsRestart"));
|
||||
|
||||
if (button == QMessageBox::Yes) {
|
||||
restart = true;
|
||||
main->close();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}; // namespace OBS
|
||||
78
frontend/plugin-manager/PluginManager.hpp
Normal file
78
frontend/plugin-manager/PluginManager.hpp
Normal file
@ -0,0 +1,78 @@
|
||||
/******************************************************************************
|
||||
Copyright (C) 2025 by FiniteSingularity <finitesingularityttv@gmail.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/>.
|
||||
******************************************************************************/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <obs-module.h>
|
||||
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace OBS {
|
||||
|
||||
struct ModuleInfo {
|
||||
std::string display_name;
|
||||
std::string module_name;
|
||||
std::string id;
|
||||
std::string version;
|
||||
bool enabled;
|
||||
bool enabledAtLaunch;
|
||||
std::vector<std::string> sources;
|
||||
std::vector<std::string> outputs;
|
||||
std::vector<std::string> encoders;
|
||||
std::vector<std::string> services;
|
||||
std::vector<std::string> sourcesLoaded;
|
||||
std::vector<std::string> outputsLoaded;
|
||||
std::vector<std::string> encodersLoaded;
|
||||
std::vector<std::string> servicesLoaded;
|
||||
};
|
||||
|
||||
class PluginManager {
|
||||
private:
|
||||
std::vector<ModuleInfo> modules_ = {};
|
||||
std::vector<std::string> disabledSources_ = {};
|
||||
std::vector<std::string> disabledOutputs_ = {};
|
||||
std::vector<std::string> disabledServices_ = {};
|
||||
std::vector<std::string> disabledEncoders_ = {};
|
||||
std::filesystem::path getConfigFilePath_();
|
||||
void loadModules_();
|
||||
void saveModules_();
|
||||
void disableModules_();
|
||||
void addModuleTypes_();
|
||||
void linkUnloadedModules_();
|
||||
|
||||
public:
|
||||
void preLoad();
|
||||
void postLoad();
|
||||
void open();
|
||||
|
||||
friend void addModuleToPluginManagerImpl(void *param, obs_module_t *newModule);
|
||||
};
|
||||
|
||||
void addModuleToPluginManagerImpl(void *param, obs_module_t *newModule);
|
||||
|
||||
}; // namespace OBS
|
||||
|
||||
// Anonymous namespace function to add module to plugin manager
|
||||
// via libobs's module enumeration.
|
||||
namespace {
|
||||
inline void addModuleToPluginManager(void *param, obs_module_t *newModule)
|
||||
{
|
||||
OBS::addModuleToPluginManagerImpl(param, newModule);
|
||||
}
|
||||
} // namespace
|
||||
79
frontend/plugin-manager/PluginManagerWindow.cpp
Normal file
79
frontend/plugin-manager/PluginManagerWindow.cpp
Normal file
@ -0,0 +1,79 @@
|
||||
/******************************************************************************
|
||||
Copyright (C) 2025 by FiniteSingularity <finitesingularityttv@gmail.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 "PluginManagerWindow.hpp"
|
||||
|
||||
#include <OBSApp.hpp>
|
||||
|
||||
#include <QCheckBox>
|
||||
#include <QDialogButtonBox>
|
||||
#include <QHBoxLayout>
|
||||
#include <QLabel>
|
||||
#include <QScrollArea>
|
||||
#include <QString>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
#include "moc_PluginManagerWindow.cpp"
|
||||
|
||||
namespace OBS {
|
||||
|
||||
PluginManagerWindow::PluginManagerWindow(std::vector<ModuleInfo> const &modules, QWidget *parent)
|
||||
: QDialog(parent),
|
||||
modules_(modules),
|
||||
ui(new Ui::PluginManagerWindow)
|
||||
{
|
||||
setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint);
|
||||
|
||||
ui->setupUi(this);
|
||||
|
||||
std::sort(modules_.begin(), modules_.end(), [](const ModuleInfo &a, const ModuleInfo &b) {
|
||||
std::string aName = !a.display_name.empty() ? a.display_name : a.module_name;
|
||||
std::string bName = !b.display_name.empty() ? b.display_name : b.module_name;
|
||||
return aName < bName;
|
||||
});
|
||||
|
||||
for (auto &metadata : modules_) {
|
||||
std::string id = metadata.module_name;
|
||||
// Check if the module is missing:
|
||||
bool missing = !obs_get_module(id.c_str()) && !obs_get_disabled_module(id.c_str());
|
||||
|
||||
QString name = !metadata.display_name.empty() ? metadata.display_name.c_str()
|
||||
: metadata.module_name.c_str();
|
||||
if (missing) {
|
||||
name += " " + QTStr("PluginManager.MissingPlugin");
|
||||
}
|
||||
auto item = new QListWidgetItem(name);
|
||||
item->setFlags(item->flags() | Qt::ItemIsUserCheckable);
|
||||
item->setCheckState(metadata.enabled ? Qt::Checked : Qt::Unchecked);
|
||||
|
||||
if (missing) {
|
||||
item->setFlags(item->flags() & ~Qt::ItemIsEnabled);
|
||||
}
|
||||
ui->modulesList->addItem(item);
|
||||
}
|
||||
|
||||
connect(ui->modulesList, &QListWidget::itemChanged, this, [this](QListWidgetItem *item) {
|
||||
auto row = ui->modulesList->row(item);
|
||||
bool checked = item->checkState() == Qt::Checked;
|
||||
modules_[row].enabled = checked;
|
||||
});
|
||||
|
||||
connect(ui->buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept);
|
||||
connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
|
||||
}
|
||||
|
||||
}; // namespace OBS
|
||||
40
frontend/plugin-manager/PluginManagerWindow.hpp
Normal file
40
frontend/plugin-manager/PluginManagerWindow.hpp
Normal file
@ -0,0 +1,40 @@
|
||||
/******************************************************************************
|
||||
Copyright (C) 2025 by FiniteSingularity <finitesingularityttv@gmail.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/>.
|
||||
******************************************************************************/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "ui_PluginManagerWindow.h"
|
||||
#include "PluginManager.hpp"
|
||||
|
||||
#include <QDialog>
|
||||
#include <QWidget>
|
||||
|
||||
namespace OBS {
|
||||
|
||||
class PluginManagerWindow : public QDialog {
|
||||
Q_OBJECT
|
||||
std::unique_ptr<Ui::PluginManagerWindow> ui;
|
||||
|
||||
public:
|
||||
explicit PluginManagerWindow(std::vector<ModuleInfo> const &modules, QWidget *parent = nullptr);
|
||||
inline std::vector<ModuleInfo> const result() { return modules_; }
|
||||
|
||||
private:
|
||||
std::vector<ModuleInfo> modules_;
|
||||
};
|
||||
|
||||
}; // namespace OBS
|
||||
@ -22,8 +22,11 @@
|
||||
#include "ColorSelect.hpp"
|
||||
#include "OBSBasicControls.hpp"
|
||||
#include "OBSBasicStats.hpp"
|
||||
#include "plugin-manager/PluginManager.hpp"
|
||||
#include "VolControl.hpp"
|
||||
|
||||
#include <obs-module.h>
|
||||
|
||||
#ifdef YOUTUBE_ENABLED
|
||||
#include <docks/YouTubeAppDock.hpp>
|
||||
#endif
|
||||
@ -184,9 +187,9 @@ static void SetSafeModuleNames()
|
||||
return;
|
||||
#else
|
||||
string module;
|
||||
stringstream modules(SAFE_MODULES);
|
||||
stringstream modules_(SAFE_MODULES);
|
||||
|
||||
while (getline(modules, module, '|')) {
|
||||
while (getline(modules_, module, '|')) {
|
||||
/* When only disallowing third-party plugins, still add
|
||||
* "unsafe" bundled modules to the safe list. */
|
||||
if (disable_3p_plugins || !unsafe_modules.count(module))
|
||||
@ -195,6 +198,24 @@ static void SetSafeModuleNames()
|
||||
#endif
|
||||
}
|
||||
|
||||
static void SetCoreModuleNames()
|
||||
{
|
||||
#ifndef SAFE_MODULES
|
||||
throw "SAFE_MODULES not defined";
|
||||
#else
|
||||
std::string safeModules = SAFE_MODULES;
|
||||
if (safeModules.empty()) {
|
||||
throw "SAFE_MODULES is empty";
|
||||
}
|
||||
string module;
|
||||
stringstream modules_(SAFE_MODULES);
|
||||
|
||||
while (getline(modules_, module, '|')) {
|
||||
obs_add_core_module(module.c_str());
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
extern void setupDockAction(QDockWidget *dock);
|
||||
|
||||
OBSBasic::OBSBasic(QWidget *parent) : OBSMainWindow(parent), undo_s(ui), ui(new Ui::OBSBasic)
|
||||
@ -996,26 +1017,23 @@ void OBSBasic::OBSInit()
|
||||
#endif
|
||||
struct obs_module_failure_info mfi;
|
||||
|
||||
/* Safe Mode disables third-party plugins so we don't need to add earch
|
||||
* paths outside the OBS bundle/installation. */
|
||||
// Safe Mode disables third-party plugins so we don't need to add each path outside the OBS bundle/installation.
|
||||
if (safe_mode || disable_3p_plugins) {
|
||||
SetSafeModuleNames();
|
||||
} else {
|
||||
AddExtraModulePaths();
|
||||
}
|
||||
|
||||
// Core modules are not allowed to be disabled by the user via plugin manager.
|
||||
SetCoreModuleNames();
|
||||
|
||||
/* Modules can access frontend information (i.e. profile and scene collection data) during their initialization, and some modules (e.g. obs-websockets) are known to use the filesystem location of the current profile in their own code.
|
||||
|
||||
Thus the profile and scene collection discovery needs to happen before any access to that information (but after intializing global settings) to ensure legacy code gets valid path information.
|
||||
*/
|
||||
RefreshSceneCollections(true);
|
||||
|
||||
blog(LOG_INFO, "---------------------------------");
|
||||
obs_load_all_modules2(&mfi);
|
||||
blog(LOG_INFO, "---------------------------------");
|
||||
obs_log_loaded_modules();
|
||||
blog(LOG_INFO, "---------------------------------");
|
||||
obs_post_load_modules();
|
||||
App()->loadAppModules(mfi);
|
||||
|
||||
BPtr<char *> failed_modules = mfi.failed_modules;
|
||||
|
||||
@ -2070,3 +2088,8 @@ OBSPromptResult OBSBasic::PromptForName(const OBSPromptRequest &request, const O
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
void OBSBasic::on_actionOpenPluginManager_triggered()
|
||||
{
|
||||
App()->pluginManagerOpenDialog();
|
||||
}
|
||||
|
||||
@ -762,6 +762,9 @@ public:
|
||||
private slots:
|
||||
void ResizeOutputSizeOfSource();
|
||||
|
||||
private slots:
|
||||
void on_actionOpenPluginManager_triggered();
|
||||
|
||||
/* -------------------------------------
|
||||
* MARK: - OBSBasic_Preview
|
||||
* -------------------------------------
|
||||
|
||||
Loading…
Reference in New Issue
Block a user