mirror of
https://github.com/WarmUpTill/SceneSwitcher.git
synced 2026-08-11 19:58:49 -05:00
Add support for inline scripts
* Script can be defined in the macro segment or loaded from a file * Supports both LUA and Python * Can be used for actions and conditions * obs_script_create and obs_script_destroy are resolved at runtime (Let's hope the API remains stable)
This commit is contained in:
344
plugins/scripting/utils/inline-script.cpp
Normal file
344
plugins/scripting/utils/inline-script.cpp
Normal file
@@ -0,0 +1,344 @@
|
||||
#include "inline-script.hpp"
|
||||
#include "log-helper.hpp"
|
||||
#include "obs-module-helper.hpp"
|
||||
|
||||
#include <obs-module.h>
|
||||
#include <obs.hpp>
|
||||
|
||||
#include <QDir>
|
||||
#include <QFileInfo>
|
||||
|
||||
namespace advss {
|
||||
|
||||
static constexpr std::string_view signalName = "advss_run_temp_script";
|
||||
|
||||
std::atomic_uint64_t InlineScript::_instanceIdCounter = 0;
|
||||
const std::string_view InlineScript::_defaultPythonScript =
|
||||
"import obspython as obs\n"
|
||||
"\n"
|
||||
"def run():\n"
|
||||
" obs.script_log(obs.LOG_WARNING, \"Hello from Python!\")\n"
|
||||
" return True\n";
|
||||
const std::string_view InlineScript::_defaultLUAScript =
|
||||
"obs = obslua\n"
|
||||
"\n"
|
||||
"function run()\n"
|
||||
" obs.script_log(obs.LOG_WARNING, \"Hello from LUA!\")\n"
|
||||
" return true\n"
|
||||
"end";
|
||||
;
|
||||
|
||||
static bool setup()
|
||||
{
|
||||
auto sh = obs_get_signal_handler();
|
||||
auto signalDecl =
|
||||
std::string("void ") + signalName.data() + "(string id)";
|
||||
signal_handler_add(sh, signalDecl.c_str());
|
||||
|
||||
return true;
|
||||
}
|
||||
static bool setupDone = setup();
|
||||
|
||||
static void cleanupScriptFile(const std::string &path)
|
||||
{
|
||||
const QFileInfo fileInfo(QString::fromStdString(path));
|
||||
if (!fileInfo.isFile()) {
|
||||
return;
|
||||
}
|
||||
QFile file(fileInfo.absoluteFilePath());
|
||||
if (!file.remove()) {
|
||||
vblog(LOG_INFO, "failed to clean up script file %s",
|
||||
fileInfo.absoluteFilePath().toStdString().c_str());
|
||||
}
|
||||
}
|
||||
|
||||
static std::optional<std::string>
|
||||
getScriptTempFilePath(obs_script_lang language)
|
||||
{
|
||||
static int counter = 0;
|
||||
++counter;
|
||||
static const QString filenamePattern =
|
||||
"scripting/advss-tmp-script%1.%2";
|
||||
const QString filename = filenamePattern.arg(counter).arg(
|
||||
language == OBS_SCRIPT_LANG_PYTHON ? "py" : "lua");
|
||||
auto settingsFile =
|
||||
obs_module_config_path(filename.toStdString().c_str());
|
||||
|
||||
if (!settingsFile) {
|
||||
blog(LOG_WARNING,
|
||||
"could not create temp script file! (obs_module_config_path)");
|
||||
return {};
|
||||
}
|
||||
|
||||
std::string path = settingsFile;
|
||||
bfree(settingsFile);
|
||||
return path;
|
||||
}
|
||||
|
||||
static bool createScriptFile(const char *settingsFile, const char *content)
|
||||
{
|
||||
const QFileInfo fileInfo(settingsFile);
|
||||
const QString dirPath = fileInfo.absolutePath();
|
||||
const QDir dir(dirPath);
|
||||
if (!dir.exists() && !dir.mkpath(dirPath)) {
|
||||
blog(LOG_WARNING, "could not create script file! (mkpath)");
|
||||
return false;
|
||||
}
|
||||
|
||||
QFile file(settingsFile);
|
||||
if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
auto out = QTextStream(&file);
|
||||
out << content;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
InlineScript::InlineScript() : _instanceId(_instanceIdCounter++)
|
||||
{
|
||||
Setup();
|
||||
}
|
||||
|
||||
InlineScript::InlineScript(const InlineScript &other)
|
||||
: _language(other._language),
|
||||
_textPython(other._textPython),
|
||||
_textLUA(other._textLUA),
|
||||
_instanceId(_instanceIdCounter++)
|
||||
{
|
||||
Setup();
|
||||
}
|
||||
|
||||
void InlineScript::Save(obs_data_t *data) const
|
||||
{
|
||||
OBSDataAutoRelease obj = obs_data_create();
|
||||
obs_data_set_int(obj, "type", _type);
|
||||
obs_data_set_int(obj, "language", _language);
|
||||
_textPython.Save(obj, "scriptPython");
|
||||
_textLUA.Save(obj, "scriptLUA");
|
||||
obs_data_set_string(obj, "file", _file.c_str());
|
||||
obs_data_set_obj(data, "script", obj);
|
||||
}
|
||||
|
||||
void InlineScript::Load(obs_data_t *data)
|
||||
{
|
||||
OBSDataAutoRelease obj = obs_data_get_obj(data, "script");
|
||||
_type = static_cast<Type>(obs_data_get_int(obj, "type"));
|
||||
_language =
|
||||
static_cast<obs_script_lang>(obs_data_get_int(obj, "language"));
|
||||
_textPython.Load(obj, "scriptPython");
|
||||
_textLUA.Load(obj, "scriptLUA");
|
||||
_file = obs_data_get_string(obj, "file");
|
||||
Setup();
|
||||
}
|
||||
|
||||
void InlineScript::SetType(Type type)
|
||||
{
|
||||
_type = type;
|
||||
Setup();
|
||||
}
|
||||
|
||||
void InlineScript::SetLanguage(obs_script_lang language)
|
||||
{
|
||||
_language = language;
|
||||
Setup();
|
||||
}
|
||||
|
||||
void InlineScript::SetText(const std::string &text)
|
||||
{
|
||||
switch (_language) {
|
||||
case OBS_SCRIPT_LANG_UNKNOWN:
|
||||
break;
|
||||
case OBS_SCRIPT_LANG_LUA:
|
||||
_textLUA = text;
|
||||
break;
|
||||
case OBS_SCRIPT_LANG_PYTHON:
|
||||
_textPython = text;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
Setup();
|
||||
}
|
||||
|
||||
const StringVariable &InlineScript::GetText() const
|
||||
{
|
||||
static const StringVariable defaultRet;
|
||||
|
||||
switch (_language) {
|
||||
case OBS_SCRIPT_LANG_UNKNOWN:
|
||||
break;
|
||||
case OBS_SCRIPT_LANG_LUA:
|
||||
return _textLUA;
|
||||
case OBS_SCRIPT_LANG_PYTHON:
|
||||
return _textPython;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return defaultRet;
|
||||
}
|
||||
|
||||
void InlineScript::SetPath(const std::string &path)
|
||||
{
|
||||
_file = path;
|
||||
Setup();
|
||||
}
|
||||
|
||||
bool InlineScript::Run()
|
||||
{
|
||||
static auto sh = obs_get_signal_handler();
|
||||
|
||||
if (_type == INLINE && _lastResolvedText != std::string(GetText())) {
|
||||
Setup();
|
||||
}
|
||||
|
||||
if (_type == FILE && _lastPath != _file) {
|
||||
Setup();
|
||||
}
|
||||
|
||||
auto cd = calldata_create();
|
||||
calldata_set_string(cd, "id", GetID().c_str());
|
||||
signal_handler_signal(sh, signalName.data(), cd);
|
||||
bool result = calldata_bool(cd, "result");
|
||||
calldata_destroy(cd);
|
||||
return result;
|
||||
}
|
||||
|
||||
void InlineScript::ResolveVariablesToFixedValues()
|
||||
{
|
||||
_textPython.ResolveVariables();
|
||||
_textLUA.ResolveVariables();
|
||||
}
|
||||
|
||||
static std::string preprocessScriptText(const std::string &text,
|
||||
obs_script_lang language,
|
||||
const std::string &id)
|
||||
{
|
||||
const std::string footerPython =
|
||||
std::string("\n\n"
|
||||
"## AUTO GENERATED ##\n"
|
||||
"def script_load(settings):\n"
|
||||
" def run_wrapper(data):\n"
|
||||
" id = obs.calldata_string(data, \"id\")\n"
|
||||
" if id == \"") +
|
||||
id +
|
||||
"\":\n"
|
||||
" ret = run()\n"
|
||||
" obs.calldata_set_bool(data, \"result\", ret)\n"
|
||||
" sh = obs.obs_get_signal_handler()\n"
|
||||
" obs.signal_handler_connect(sh, \"" +
|
||||
signalName.data() + "\", run_wrapper)\n\n";
|
||||
|
||||
const std::string footerLUA =
|
||||
std::string(
|
||||
"\n\n"
|
||||
"-- AUTO GENERATED --\n"
|
||||
"function script_load(settings)\n"
|
||||
" local run_wrapper = (function(data)\n"
|
||||
" local id = obs.calldata_string(data, \"id\")\n"
|
||||
" if id == \"") +
|
||||
id +
|
||||
"\" then\n"
|
||||
" local ret = run()\n"
|
||||
" obs.calldata_set_bool(data, \"result\", ret)\n"
|
||||
" end\n"
|
||||
" end)\n"
|
||||
" local sh = obs.obs_get_signal_handler()\n"
|
||||
" obs.signal_handler_connect(sh, \"" +
|
||||
signalName.data() +
|
||||
"\" , run_wrapper)\n"
|
||||
"end\n";
|
||||
|
||||
std::string scriptText =
|
||||
language == OBS_SCRIPT_LANG_PYTHON ? footerPython : footerLUA;
|
||||
return text + scriptText;
|
||||
}
|
||||
|
||||
void InlineScript::SetupFile()
|
||||
{
|
||||
const auto path = GetLUACompatiblePath(_file);
|
||||
_fileId = path;
|
||||
|
||||
if (path.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!QFileInfo(QString::fromStdString(path)).exists()) {
|
||||
const auto text = preprocessScriptText(
|
||||
_language == OBS_SCRIPT_LANG_PYTHON
|
||||
? _defaultPythonScript.data()
|
||||
: _defaultLUAScript.data(),
|
||||
_language, GetID());
|
||||
(void)createScriptFile(_file.c_str(), text.c_str());
|
||||
}
|
||||
|
||||
_script = std::unique_ptr<obs_script_t, ScriptDeleter>(
|
||||
CreateOBSScript(path.c_str(), nullptr), {});
|
||||
_lastPath = _file;
|
||||
}
|
||||
|
||||
void InlineScript::SetupInline()
|
||||
{
|
||||
const StringVariable &text =
|
||||
_language == OBS_SCRIPT_LANG_PYTHON ? _textPython : _textLUA;
|
||||
const auto scriptText = preprocessScriptText(text, _language, GetID());
|
||||
|
||||
auto path_ = getScriptTempFilePath(_language);
|
||||
if (!path_) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto path = GetLUACompatiblePath(*path_);
|
||||
if (!createScriptFile(path.c_str(), scriptText.c_str())) {
|
||||
return;
|
||||
}
|
||||
|
||||
_script = std::unique_ptr<obs_script_t, ScriptDeleter>(
|
||||
CreateOBSScript(path.c_str(), nullptr), {path});
|
||||
_lastResolvedText = text;
|
||||
}
|
||||
|
||||
void InlineScript::Setup()
|
||||
{
|
||||
_script.reset();
|
||||
_lastResolvedText = "";
|
||||
_lastPath = "";
|
||||
|
||||
if (_type == FILE) {
|
||||
SetupFile();
|
||||
} else {
|
||||
SetupInline();
|
||||
}
|
||||
}
|
||||
|
||||
std::string InlineScript::GetID() const
|
||||
{
|
||||
if (_type == FILE) {
|
||||
return _fileId;
|
||||
}
|
||||
return std::to_string(_instanceId);
|
||||
}
|
||||
|
||||
void InlineScript::ScriptDeleter::operator()(obs_script_t *script)
|
||||
{
|
||||
DestroyOBSScript(script);
|
||||
if (!tempScriptPath.empty()) {
|
||||
cleanupScriptFile(tempScriptPath);
|
||||
}
|
||||
}
|
||||
|
||||
ScriptEditor::ScriptEditor(QWidget *parent) : VariableTextEdit(parent, 15, 5)
|
||||
{
|
||||
installEventFilter(this);
|
||||
}
|
||||
|
||||
bool ScriptEditor::eventFilter(QObject *obj, QEvent *event)
|
||||
{
|
||||
if (event->type() == QEvent::FocusOut) {
|
||||
emit ScriptChanged();
|
||||
}
|
||||
return QObject::eventFilter(obj, event);
|
||||
}
|
||||
|
||||
} // namespace advss
|
||||
76
plugins/scripting/utils/inline-script.hpp
Normal file
76
plugins/scripting/utils/inline-script.hpp
Normal file
@@ -0,0 +1,76 @@
|
||||
#pragma once
|
||||
#include "obs-script-helpers.hpp"
|
||||
#include "variable-text-edit.hpp"
|
||||
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
#include <obs-data.h>
|
||||
|
||||
namespace advss {
|
||||
|
||||
class InlineScript {
|
||||
public:
|
||||
InlineScript();
|
||||
InlineScript(const InlineScript &);
|
||||
|
||||
enum Type { INLINE, FILE };
|
||||
|
||||
void Save(obs_data_t *) const;
|
||||
void Load(obs_data_t *);
|
||||
|
||||
void SetType(Type);
|
||||
Type GetType() const { return _type; }
|
||||
void SetLanguage(obs_script_lang);
|
||||
obs_script_lang GetLanguage() const { return _language; }
|
||||
void SetText(const std::string &);
|
||||
const StringVariable &GetText() const;
|
||||
void SetPath(const std::string &);
|
||||
const std::string &GetPath() const { return _file; }
|
||||
|
||||
bool Run();
|
||||
|
||||
void ResolveVariablesToFixedValues();
|
||||
|
||||
private:
|
||||
void Setup();
|
||||
void SetupFile();
|
||||
void SetupInline();
|
||||
std::string GetID() const;
|
||||
|
||||
Type _type = INLINE;
|
||||
obs_script_lang _language = OBS_SCRIPT_LANG_PYTHON;
|
||||
std::string _file;
|
||||
StringVariable _textPython = _defaultPythonScript.data();
|
||||
StringVariable _textLUA = _defaultLUAScript.data();
|
||||
std::string _lastResolvedText;
|
||||
std::string _lastPath;
|
||||
std::string _fileId;
|
||||
const uint64_t _instanceId;
|
||||
|
||||
struct ScriptDeleter {
|
||||
void operator()(obs_script_t *);
|
||||
std::string tempScriptPath;
|
||||
};
|
||||
std::unique_ptr<obs_script_t, ScriptDeleter> _script;
|
||||
|
||||
static std::atomic_uint64_t _instanceIdCounter;
|
||||
static const std::string_view _defaultPythonScript;
|
||||
static const std::string_view _defaultLUAScript;
|
||||
};
|
||||
|
||||
class ScriptEditor : public VariableTextEdit {
|
||||
Q_OBJECT
|
||||
public:
|
||||
ScriptEditor(QWidget *parent);
|
||||
|
||||
signals:
|
||||
void ScriptChanged();
|
||||
|
||||
private:
|
||||
bool eventFilter(QObject *obj, QEvent *event) override;
|
||||
};
|
||||
|
||||
} // namespace advss
|
||||
72
plugins/scripting/utils/obs-script-helpers.cpp
Normal file
72
plugins/scripting/utils/obs-script-helpers.cpp
Normal file
@@ -0,0 +1,72 @@
|
||||
#include "obs-script-helpers.hpp"
|
||||
#include "log-helper.hpp"
|
||||
|
||||
#include <QFileInfo>
|
||||
#include <QLibrary>
|
||||
|
||||
namespace advss {
|
||||
|
||||
static const char *libName =
|
||||
#if defined(WIN32)
|
||||
"obs-scripting.dll";
|
||||
#elif __APPLE__
|
||||
"obs-scripting.dylib";
|
||||
#else
|
||||
"obs-scripting.so";
|
||||
#endif
|
||||
|
||||
typedef obs_script_t *(*obs_script_create_t)(const char *, obs_data_t *);
|
||||
typedef void (*obs_script_destroy_t)(obs_script_t *);
|
||||
|
||||
obs_script_create_t obs_script_create = nullptr;
|
||||
obs_script_destroy_t obs_script_destroy = nullptr;
|
||||
|
||||
static bool setup()
|
||||
{
|
||||
QLibrary scriptingLib(libName);
|
||||
|
||||
obs_script_create =
|
||||
(obs_script_create_t)scriptingLib.resolve("obs_script_create");
|
||||
if (!obs_script_create) {
|
||||
blog(LOG_WARNING,
|
||||
"could not resolve obs_script_create symbol!");
|
||||
}
|
||||
|
||||
obs_script_destroy = (obs_script_destroy_t)scriptingLib.resolve(
|
||||
"obs_script_destroy");
|
||||
if (!obs_script_destroy) {
|
||||
blog(LOG_WARNING,
|
||||
"could not resolve obs_script_destroy symbol!");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
static bool setupDone = setup();
|
||||
|
||||
obs_script_t *CreateOBSScript(const char *path, obs_data_t *settings)
|
||||
{
|
||||
if (!obs_script_create) {
|
||||
return nullptr;
|
||||
}
|
||||
return obs_script_create(path, settings);
|
||||
}
|
||||
|
||||
void DestroyOBSScript(obs_script_t *script)
|
||||
{
|
||||
if (!obs_script_destroy) {
|
||||
return;
|
||||
}
|
||||
obs_script_destroy(script);
|
||||
}
|
||||
|
||||
std::string GetLUACompatiblePath(const std::string &path)
|
||||
{
|
||||
// Can't use settingsFile here as LUA will complain if Windows style
|
||||
// paths (C:\some\path) are used.
|
||||
// QFileInfo::absoluteFilePath will convert those paths so LUA won't
|
||||
// complain. (C:/some/path)
|
||||
const QFileInfo fileInfo(QString::fromStdString(path));
|
||||
return fileInfo.absoluteFilePath().toStdString();
|
||||
}
|
||||
|
||||
} // namespace advss
|
||||
23
plugins/scripting/utils/obs-script-helpers.hpp
Normal file
23
plugins/scripting/utils/obs-script-helpers.hpp
Normal file
@@ -0,0 +1,23 @@
|
||||
#pragma once
|
||||
#include <obs-data.h>
|
||||
|
||||
#include <string>
|
||||
|
||||
// Based on obs-scripting.h
|
||||
|
||||
struct obs_script;
|
||||
typedef struct obs_script obs_script_t;
|
||||
|
||||
enum obs_script_lang {
|
||||
OBS_SCRIPT_LANG_UNKNOWN,
|
||||
OBS_SCRIPT_LANG_LUA,
|
||||
OBS_SCRIPT_LANG_PYTHON
|
||||
};
|
||||
|
||||
namespace advss {
|
||||
|
||||
obs_script_t *CreateOBSScript(const char *path, obs_data_t *settings);
|
||||
void DestroyOBSScript(obs_script_t *script);
|
||||
std::string GetLUACompatiblePath(const std::string &path);
|
||||
|
||||
} // namespace advss
|
||||
Reference in New Issue
Block a user