function setup(settings) -- [[ UPSTREAMX FILTER DEFINITION ]] local upstreamX= obs.script.filter({ name="UpstreamX (vs.2.1.0)", id="upstreamx_filter_v2", }) function upstreamX.setup(src) upstreamX.update(src) obs.register.event(function(event) if event == obslua.OBS_FRONTEND_EVENT_RECORDING_STARTED or event == obslua.OBS_FRONTEND_EVENT_STREAMING_STARTED then if src.mode ~= "mode4" then upstreamX.update(src) end elseif event == obslua.OBS_FRONTEND_EVENT_RECORDING_STOPPED or event == obslua.OBS_FRONTEND_EVENT_STREAMING_STOPPED then if src.mode ~= "mode4" then upstreamX.update(src) end end end) end function upstreamX.update(src) src.imp= src.settings.str("mode_time") src.time=0 src.mode= src.settings.str("mode") src.clock= os.clock() src.isFinished= false src.isFirstTime= true src.mini_mode= src.settings.bul("mini_mode") if src.imp == "mode2" then local custom_time = src.settings.str("custom_time_input") local h, m, s = custom_time:match("^(%d%d?):(%d%d):(%d%d)$") if h and m and s then src.time = tonumber(h) * 3600 + tonumber(m) * 60 + tonumber(s) else src.time = nil end end if src.mode == "mode4" then hotkey_setup(src) end if src.item and src.item.data then return src.item.text(format_time(src.time, src.mini_mode)) end end function upstreamX.finally(src) src.item= obs.front.source(src.source) if not src.item or not src.item.data then return end src.item.show() upstreamX.update(src) end function upstreamX.destroy(src) if src.item and src.item.data then src.item.free() end end function upstreamX.tick(src) if src.isFirstTime and src.item and src.item.data then src.item.text(format_time(src.time)) src.isFirstTime= false end if src.isFinished or not src.isInitialized then return end local is_ok= false if src.mode == "mode1" then if obslua.obs_frontend_streaming_active() or obslua.obs_frontend_recording_active() then is_ok = true end elseif src.mode == "mode2" then if obslua.obs_frontend_streaming_active() then is_ok = true end elseif src.mode == "mode3" then if obslua.obs_frontend_recording_active() then is_ok = true end else if src.HotKeyStart then is_ok = true end end if not is_ok then return end if os.clock() - src.clock < 1 then return end src.clock = os.clock() if src.imp == "mode1" then src.time = src.time + 1 elseif src.imp == "mode2" then if src.time and src.time > 0 then src.time = src.time - 1 end if src.time and src.time <= 0 then src.time = 0 src.isFinished = true do_blink(src.item) elseif src.time == nil then src.isFinished = true src.time=0 do_blink(src.item) end end if src.item and src.item.data then src.item.text(format_time(src.time, src.mini_mode)) end end function upstreamX.defaults(settings) settings.str("mode", "mode1", true) settings.str("mode_time", "mode1", true) settings.str("info_label", "", true) settings.str("custom_time_input", "00:00:00", true) end function upstreamX.properties(src) local p= obs.script.create() local mini_mode= obs.script.bool(p, "mini_mode", "Minimal mode") mini_mode.hint("Minimal time formatting without hours (MM:SS or SS) will be used when time is under 1 hour") local mode= obs.script.options(p, "mode") mode.add.str("Time will activate when streaming or recording", "mode1") mode.add.str("Time will activate when streaming", "mode2") mode.add.str("Time will activate when recording", "mode3") mode.add.str("Time will start/stop when you press specific hotkey", "mode4") local info_label= obs.script.label(p, "info_label", "") info_label.hide() -- [[ Timer System ]] local mode_time= obs.script.options(p, "mode_time") mode_time.add.str("Time will count upwards", "mode1") mode_time.add.str("Time will count downwards", "mode2") local custom_time_input= obs.script.input(p, "custom_time_input", "Time: ") custom_time_input.hint("Enter time in HH:MM:SS format") custom_time_input.hide() -- [[ TIME MODE CHANGE CALLBACK ]] mode_time.onchange(function(value) custom_time_input.hide() if value == "mode2" then custom_time_input.show() end return true end) -- [[ MODE CHANGE CALLBACK ]] mode.onchange(function(value) info_label.hide() if value == "mode4" then info_label.show() info_label.text("Please set hotkeys in OBS settings -> Hotkeys") hotkey_setup(src) -- [[ REGISTER HOTKEY ]] else -- [[ REMOVE HOTKEY ]] if src and src.item and src.item.data then local hotkey_unique_name1 = src.item.get_name() .. "_start" local hotkey_unique_name2 = src.item.get_name() .. "_stop" local hotkey_start= obs.register.get_hotkey(hotkey_unique_name1) if hotkey_start then hotkey_start.remove(true) end local hotkey_stop= obs.register.get_hotkey(hotkey_unique_name2) if hotkey_stop then hotkey_stop.remove(true) end end end return true end) if src.settings.str("mode") == "mode4" then hotkey_setup(src) info_label.show() info_label.text("Please set hotkeys in OBS settings -> Hotkeys") end if src.settings.str("mode_time") == "mode2" then custom_time_input.show() end return p end -- [[ UPSTREAMX FILTER DEFINITION END ]] -- [[ EVENTS ]] -- [[ Functions ]] function format_time(time, mini) -- seconds if minutes and hours are 0, minutes if hours are 0 -- hours are greater than 24 will be displayed as days, hours, minutes and seconds if not time then return "00:00:00" end local hours = math.floor(time / 3600) local minutes = math.floor((time % 3600) / 60) local seconds = math.floor(time % 60) if mini then if hours >= 24 then local days = math.floor(hours / 24) hours = hours % 24 return string.format("%dd %02dh %02dm %02ds", days, hours, minutes, seconds) elseif hours > 0 then return string.format("%02d:%02d:%02d", hours, minutes, seconds) elseif minutes > 0 then return string.format("%02d:%02d", minutes, seconds) else return string.format("%02d", seconds) end else return string.format("%02d:%02d:%02d", hours, minutes, seconds) end end function hotkey_setup(src) if src and src.item and src.item.data and not src.has_hotkey then src.has_hotkey= true local source_name= src.item.get_name() obs.register.hotkey(source_name .. "_upstreamx_start", "[UpstreamX (" .. source_name .. ") ] START",function(active) if active and src.mode == "mode4" then src.clock= os.clock() src.HotKeyStart= true src.isFinished= false end end) obs.register.hotkey(source_name .. "_upstreamx_stop", "[UpstreamX (" .. source_name .. ") ] STOP",function(active) if active and src.mode == "mode4" then src.HotKeyStart= false src.isFinished= true end end) obs.register.hotkey(source_name .. "_upstreamx_reset", "[UpstreamX (" .. source_name .. ") ] RESET",function(active) if active and src.mode == "mode4" then src.HotKeyStart= true src.clock= os.clock() local is_finished_before= src.isFinished upstreamX.update(src) src.isFinished= is_finished_before end end) end end end function do_blink(item, count) if not item or not item.data then return end if count == nil then count = 6 end if count <= 0 then return end item.hide(); return obs.time.schedule(function() item.show(); return obs.time.schedule(function() do_blink(item, count - 1) end).after(500) end).after(500) end -- [[ OBS CUSTOM API BEGIN ]] -- [[ OBS CUSTOM CALLBACKS ]] function script_load(settings) -- print("[OBS CUSTOM WRAPPER API]") local ck= os.clock() obs.utils.script_shutdown = false obs.utils.settings = obs.PairStack(settings, nil, nil, true) -- print("api_exit: " .. tostring(obs.utils.settings.bul("obs_custom_api_exited"))) if obs.utils.settings.get_bul("obs_custom_api_exited") == false then obs.utils.loaded=true end if setup and type(setup) == "function" then setup(obs.utils.settings) end for _, filter in pairs(obs.utils.filters) do obslua.obs_register_source(filter) end local script_is_unloading= false obslua.obs_frontend_add_event_callback(function(event_id) -- print("[OBS CUSTOM API] Frontend event triggered: " .. tostring(event_id)) if event_id == obslua.OBS_FRONTEND_EVENT_FINISHED_LOADING then -- print("[OBS CUSTOM API] Finished loading, initializing script... took " .. string.format("%.2f", (os.clock() - ck) * 1000) .. "ms") obs.utils.first_time_load= true obs.utils.loaded = true obs.utils.settings.bul("obs_custom_api_exited", false) elseif event_id == obslua.OBS_FRONTEND_EVENT_EXIT or (script_is_unloading and event_id == obslua.OBS_FRONTEND_EVENT_SCENE_COLLECTION_CLEANUP) then obs.utils.obs_closing= true elseif event_id == obslua.OBS_FRONTEND_EVENT_SCRIPTING_SHUTDOWN then script_is_unloading = true end end) obs.time.schedule(function() obs.utils.loaded = true end).after(1200) end function script_save(settings) -- print("[UNI-SAVE-IT]") obs.utils.settings.bul("obs_custom_api_exited", true) if obs.utils.script_shutdown then return end -- [[ OBS REGISTER HOTKEY SAVE DATA]] for name, iter in pairs(obs.register.hotkey_id_list) do local new_data = obslua.obs_hotkey_save(iter.id) if new_data then obs.utils.settings.arr(name, new_data) obslua.obs_data_array_release(new_data) end end -- [[ OBS REGISTER HOTKEY SAVE DATA END]] if type(onSaving) == "function" then return onSaving(obs.PairStack(settings, nil, nil, true)) end end function script_unload() obs.utils.script_shutdown = true -- if obs.utils.scheduled then -- for _, clb in pairs(obs.utils.scheduled) do -- obslua.timer_remove(clb) -- end -- obs.utils.scheduled = {} -- end -- for _, iter in pairs(obs.mem.freeup) do -- if iter and iter.data then -- iter.free() -- end -- end if obs._unload and type(obs._unload) == "table" then for _, iter in pairs(obs._unload) do if type(iter) == "table" and iter.data and iter.free then iter.free() elseif type(iter) == "function" then pcall(function() iter() end) end end end for _, event in pairs(obs.register.event_id_list) do if event and type(event.remove) == "function" then event.remove(true) end end if unset and type(unset) == "function" then return unset() end end function script_defaults(settings, pp) settings= obs.PairStack(settings, nil, nil, true) settings.bul("obs_custom_api_exited",false, true) if type(defaults) == "function" then return defaults(settings) end end function script_properties() if obs.utils.ui and type(obs.utils.ui) == "function" then return obs.utils.ui() end end -- [[ OBS CUSTOM CALLBACKS END ]] obs = { utils = { scheduled = {}, script_shutdown = false, OBS_SCENEITEM_TYPE = 1, OBS_SRC_TYPE = 2, OBS_OBJ_TYPE = 3, OBS_ARR_TYPE = 4, OBS_SCENE_TYPE = 5, OBS_SCENEITEM_LIST_TYPE = 6, OBS_SRC_LIST_TYPE = 7, OBS_UN_IN_TYPE = -1, OBS_SRC_WEAK_TYPE = 8, table = {}, expect_wrapper = {}, properties = { list = {}, options = {}, }, filters = {}, _queue = {},loaded= false,first_time_load= false, obs_closing= false }, time = {}, scene = {}, client = {}, mem = { freeup = {} }, script = {}, enum = { path = { read = obslua.OBS_PATH_FILE, write = obslua.OBS_PATH_FILE_SAVE, folder = obslua.OBS_PATH_DIRECTORY }, button = { default = obslua.OBS_BUTTON_DEFAULT, url = obslua.OBS_BUTTON_URL, }, list = { string = obslua.OBS_EDITABLE_LIST_TYPE_STRINGS, url = obslua.OBS_EDITABLE_LIST_TYPE_FILES_AND_URLS, file = obslua.OBS_EDITABLE_LIST_TYPE_FILES }, text = { error = obslua.OBS_TEXT_INFO_ERROR, default = obslua.OBS_TEXT_INFO, warn = obslua.OBS_TEXT_INFO_WARNING, input = obslua.OBS_TEXT_DEFAULT, password = obslua.OBS_TEXT_PASSWORD, textarea = obslua.OBS_TEXT_MULTILINE, }, group = { normal = obslua.OBS_GROUP_NORMAL, checked = obslua.OBS_GROUP_CHECKABLE, }, options = { string = obslua.OBS_COMBO_FORMAT_STRING, int = obslua.OBS_COMBO_FORMAT_INT, float = obslua.OBS_COMBO_FORMAT_FLOAT, bool = obslua.OBS_COMBO_FORMAT_BOOL, edit = obslua.OBS_COMBO_TYPE_EDITABLE, default = obslua.OBS_COMBO_TYPE_LIST, radio = obslua.OBS_COMBO_TYPE_RADIO, }, number = { int = obslua.OBS_COMBO_FORMAT_INT, float = obslua.OBS_COMBO_FORMAT_FLOAT, slider = 1000, input = 2000 }, bound = { none = obslua.OBS_BOUNDS_NONE, scale_inner = obslua.OBS_BOUNDS_SCALE_INNER, scale_outer = obslua.OBS_BOUNDS_SCALE_OUTER, stretch = obslua.OBS_BOUNDS_STRETCH, scale_width = obslua.OBS_BOUNDS_SCALE_WIDTH, scale_height = obslua.OBS_BOUNDS_SCALE_HEIGHT, max = obslua.OBS_BOUNDS_MAX_ONLY, } }, register = { hotkey_id_list = {}, event_id_list = {} }, front = {}, shared = {}, _unload= {} }; bit = require('bit') os = require('os') -- dkjson= require('dkjson') math.randomseed(os.time()) -- schedule an event -- [[ MEMORY MANAGE API ]] local ffi = require("ffi") function obs.shared.api(named_api) local arr_data_t = nil local function init_obs_data_t() for _, scene_name in pairs(obs.scene:names()) do local a_scene = obs.scene:get_scene(scene_name) if a_scene and a_scene.source then local s_data_t = obs.PairStack( obslua.obs_source_get_settings(a_scene.source) ) if not s_data_t or s_data_t.data == nil then a_scene.free() else if arr_data_t and arr_data_t.data then -- replace data to the current s_data_t.arr(named_api, arr_data_t.data) else -- register data to the current arr_data_t = s_data_t.arr(named_api) if not arr_data_t or arr_data_t.data == nil then arr_data_t = obs.ArrayStack() s_data_t.arr(named_api, arr_data_t.data) arr_data_t.free() arr_data_t = nil end end s_data_t.free() a_scene.free() end end end if not arr_data_t or arr_data_t.data == nil then arr_data_t = obs.ArrayStack() end end init_obs_data_t() function arr_data_t.save() init_obs_data_t() end function arr_data_t.del() local del_count = 0 for _, scene_name in pairs(obs.scene:names()) do local a_scene = obs.scene:get_scene(scene_name) if a_scene and a_scene.source then local s_data_t = obs.PairStack( obslua.obs_source_get_settings(a_scene.source) ) if not s_data_t or s_data_t.data == nil then a_scene.free() else s_data_t.del(named_api) del_count = del_count + 1 s_data_t.free() end a_scene.free() end end return del_count end -- obs.utils.table.append(obj_data_t, arr_data_t) return arr_data_t end function obs.expect(callback) return function(...) local args = { ... } local data = nil local caller = "" for i, v in ipairs(args) do if caller ~= "" then caller = caller .. "," end caller = caller .. "args[" .. tostring(i) .. "]" end caller = "return function(callback,args) return callback(" .. caller .. ") end"; local run = loadstring(caller) local success, result = pcall(function() data = run()(callback, args) end) local free_count = 0 if not success then for _, iter in pairs(obs.utils.expect_wrapper) do if iter and type(iter.free) == "function" then local s, r = pcall(function() iter.free() end) if s then free_count = free_count + 1 end end end obslua.script_log(obslua.LOG_ERROR, "[ErrorWrapper ERROR] => " .. tostring(result)) end return data end end function obs.ArrayStack(stack, name, fallback, unsafe) if fallback == nil then fallback = true end local self = nil self = { index = 0, get = function(index) if type(index) ~= "number" or index < 0 or index > self.size() then return nil end return obs.PairStack(obslua.obs_data_array_item(self.data, index), nil, true) end, next = obs.expect(function(__index) if type(self.index) ~= "number" or self.index < 0 or self.index > self.size() then return assert(false, "[ArrayStack] Invalid data provided or corrupted data for (" .. tostring(name) .. ")") end return coroutine.wrap(function() if self.size() <= 0 then return nil end local i = 0 if __index == nil or type(__index) ~= "number" or __index < 0 or __index > self.size() then __index = 0 end for i = __index, self.size() - 1 do coroutine.yield(i, obs.PairStack( obslua.obs_data_array_item(self.data, i), nil, false )) end end) -- local temp = self.index;self.index = self.index + 1 -- return obs.PairStack(obslua.obs_data_array_item(self.data, temp), nil, true) end), find = function(key, value) local index = 0 for itm in self.next() do if itm and type(itm) == "table" and itm.data then if itm.get_str(key) == value or itm.get_int(key) == value or itm.get_bul(key) == value or itm.get_dbl(key) == value then return itm, index end index = index + 1 itm.free() end end return nil, nil end, free = function() if self.data == nil or unsafe then return false end obslua.obs_data_array_release(self.data) self.data = nil return true end, insert = obs.expect(function(value) if type(value) ~= "userdata" and type(value) == "table" and value["data"] and type(value["data"]) == "userdata" then value = value.data end if value == nil or type(value) ~= "userdata" then obslua.script_log("FAILED TO INSERT OBJECT INTO [ArrayStack]") return false end obslua.obs_data_array_push_back(self.data, value) return self end), size = obs.expect(function() if self.data == nil then return 0 end return obslua.obs_data_array_count(self.data); end), rm = obs.expect(function(idx) if type(idx) ~= "number" or idx < 0 or self.size() <= 0 or idx > self.size() then obslua.script_log("FAILED TO RM DATA FROM [ArrayStack] (INVALID INDEX)") return false end obslua.obs_data_array_erase(self.data, idx) return self end) } if stack and name then self.data = obslua.obs_data_get_array(stack, name) elseif not stack and fallback then self.data = obslua.obs_data_array_create() else self.data = stack end return self end function obs.time.schedule(scheduler_callback) local self;self={ after= function(cond_or_time_ms) if type(cond_or_time_ms) == "number" then if cond_or_time_ms <= 0 then return type(scheduler_callback) == "function" and scheduler_callback() or nil else local tm = nil local tick= os.clock() tm = obs.time.tick(function(t, now) local tknow=(now - tick) * 1000 -- print("[OBS CUSTOM API SCHEDULER] Tick: " .. string.format("%.2f", tknow) .. "ms / " .. tostring(cond_or_time_ms) .. "ms") if tknow >= cond_or_time_ms then if type(scheduler_callback) == "function" then scheduler_callback() end if tm and type(tm.clear) == "function" then tm.clear() end end end) end elseif type(cond_or_time_ms) == "function" then local tm = nil tm = obs.time.tick(function() if cond_or_time_ms() then if type(scheduler_callback) == "function" then scheduler_callback() end if tm and type(tm.clear) == "function" then tm.clear() end end end) end end } return self end function obs.time.tick(fn, interval) local tm = nil local wrapper = function() if obs.utils.script_shutdown then return end return fn(tm, os.clock()) end if not interval or type(interval) ~= "number" or interval == 0 or (interval <= 0 and not interval > 0) then interval = 1 end tm = { clear = function() return obslua.timer_remove(wrapper) end } obslua.timer_add(wrapper, interval) return tm end function obs.wrap(self) if not self or self == nil then self = { type = obs.utils.OBS_UN_IN_TYPE, data = nil, item = nil } end if not self.data then self.data = self.item end if not self.item then self.item = self.data end -- Debugging name helper for k, v in pairs(obs.utils) do if v == self.type then self.type_name = tostring(k) end end function self.get_source() if not self.data then return nil end if self.type == obs.utils.OBS_SRC_TYPE then return self.data elseif self.type == obs.utils.OBS_SCENEITEM_TYPE then return obslua.obs_sceneitem_get_source(self.data) else return self.data end end function self.free() if self.released or not self.data then return end if self.unsafe then self.data = nil; self.released = true return end -- 4. Actual Release Logic if self.type == obs.utils.OBS_SCENE_TYPE then obslua.obs_scene_release(self.data) elseif self.type == obs.utils.OBS_SRC_WEAK_TYPE then obslua.obs_weak_source_release(self.data) elseif self.type == obs.utils.OBS_SRC_TYPE then obslua.obs_source_release(self.data) elseif self.type == obs.utils.OBS_ARR_TYPE then obslua.obs_data_array_release(self.data) elseif self.type == obs.utils.OBS_OBJ_TYPE then obslua.obs_data_release(self.data) elseif self.type == obs.utils.OBS_SCENEITEM_TYPE then obslua.obs_sceneitem_release(self.data) elseif self.type == obs.utils.OBS_SCENEITEM_LIST_TYPE then if type(self.data) == "table" then for _, itm in ipairs(self.data) do obslua.obs_sceneitem_release(itm) end elseif obslua.sceneitem_list_release then obslua.sceneitem_list_release(self.data) end elseif self.type == obs.utils.OBS_SRC_LIST_TYPE then if type(self.data) == "table" then for _, src_ptr in ipairs(self.data) do obslua.obs_source_release(src_ptr) end elseif obslua.source_list_release then obslua.source_list_release(self.data) end end self.data = nil; self.item = nil; self.released = true end return self end function obs.PairStack(stack, name, fallback, unsafe) if fallback == nil then fallback = true end local self = nil; self = { free = function() if self.data == nil or unsafe or obs.utils.script_shutdown then return false end obslua.obs_data_release(self.data) self.data = nil return true end, json = function(p) if not p then return obslua.obs_data_get_json(self.data) else return obslua.obs_data_get_json_pretty(self.data) end end, -- ... (rest of PairStack methods are fine) ... str = obs.expect(function(name, value, def) if name and value == nil then return self.get_str(name) end if self.data and name then if def then obslua.obs_data_set_default_string(self.data, name, value) else obslua.obs_data_set_string(self.data, name, value) end end return self end), int = obs.expect(function(name, value, def) value = tonumber(value) if name and value == nil then return self.get_int(name) end if self.data and name then if def then obslua.obs_data_set_default_int(self.data, name, value) else obslua.obs_data_set_int(self.data, name, value) end end return self end), dbl = obs.expect(function(name, value, def) value= tonumber(value) if name and value == nil then return self.get_dbl(name) end if self.data and name then if def then obslua.obs_data_set_default_double(self.data, name, value) else obslua.obs_data_set_double(self.data, name, value) end end return self end), bul = obs.expect(function(name, value, def) if name and type(value) ~= "boolean" then return self.get_bul(name) end if self.data and name then if def then obslua.obs_data_set_default_bool(self.data, name, value) else obslua.obs_data_set_bool(self.data, name, value) end end return self end), arr = obs.expect(function(name, value, def) if name and value == nil then return self.get_arr(name) end -- Unwrap wrapper if passed if type(value) ~= "userdata" and type(value) == "table" and value["data"] then value = value.data end if self.data and name and value then if def then obslua.obs_data_set_default_array(self.data, name, value) else obslua.obs_data_set_array(self.data, name, value) end end return self end), obj = obs.expect(function(name, value, def) if name and value == nil then return self.get_obj(name) end if type(value) ~= "userdata" and type(value) == "table" and value["data"] then value = value.data end if self.data and name and value then if def then obslua.obs_data_set_default_obj(self.data, name, value) else obslua.obs_data_set_obj(self.data, name, value) end end return self end), -- Getters (Simplified for brevity, logic unchanged) get_str = obs.expect(function(name, def) return def and obslua.obs_data_get_default_string(self.data, name) or obslua.obs_data_get_string(self.data, name) end), get_int = obs.expect(function(name, def) return def and obslua.obs_data_get_default_int(self.data, name) or obslua.obs_data_get_int(self.data, name) end), get_dbl = obs.expect(function(name, def) return def and obslua.obs_data_get_default_double(self.data, name) or obslua.obs_data_get_double(self.data, name) end), get_bul = obs.expect(function(name, def) if def ~= nil and type(def) ~= "boolean" then def = nil end if def ~= true then return obslua.obs_data_get_bool(self.data, name) else return obslua.obs_data_get_default_bool(self.data, name) end end), get_obj = obs.expect(function(name, def) local res = def and obslua.obs_data_get_default_obj(self.data, name) or obslua.obs_data_get_obj(self.data, name) return obs.PairStack(res, nil, false) -- Return safe wrapper end), get_arr = obs.expect(function(name, def) local res = def and obslua.obs_data_get_default_array(self.data, name) or obslua.obs_data_get_array(self.data, name) return obs.ArrayStack(res, nil, false) end), del = obs.expect(function(name) obslua.obs_data_erase(self.data, name) return true end), } if stack and name then self.data = obslua.obs_data_get_obj(stack, name) elseif not stack and fallback then -- print("[PairStack] No stack provided, creating new data object for (" .. tostring(name) .. ")") self.data = obslua.obs_data_create() else if type(stack) == "string" then self.data = obslua.obs_data_create_from_json(stack) if not self.data then -- print("[PairStack] Failed to create data from JSON string for (" .. tostring(name) .. "), creating empty data object instead.") self.data = obslua.obs_data_create() end elseif type(stack) == "userdata" then -- print("[PairStack] Using provided userdata stack for (" .. tostring(name) .. ")") self.data = stack else -- print("[PairStack] Invalid stack type provided for (" .. tostring(name) .. "), creating empty data object instead.") self.data = obslua.obs_data_create() end end return self end -- [[ MEMORY MANAGE API END ]] -- [[ OBS REGISTER CUSTOM API]] function obs.register:remove_all() for _, iter in pairs(obs.register.hotkey_id_list) do if iter and type(iter.remove) == "function" then iter.remove(true) end end obs.register.hotkey_id_list = {} for _, iter in pairs(obs.register.event_id_list) do if iter and type(iter.remove) == "function" then iter.remove(true) end end obs.register.event_id_list = {} end function obs.register:hotkey_remove() for _, iter in pairs(obs.register.hotkey_id_list) do if iter and type(iter.remove) == "function" then iter.remove(true) end end obs.register.hotkey_id_list = {} end function obs.register:event_remove() for _, iter in pairs(obs.register.event_id_list) do if iter and type(iter.remove) == "function" then iter.remove(true) end end obs.register.event_id_list = {} end function obs.register.hotkey(unique_id, title, callback) local script_path_value = script_path() unique_id = tostring(script_path_value) .. "_" .. tostring(unique_id) local hotkey_id = obslua.obs_hotkey_register_frontend( unique_id, title, callback ) -- load from data local hotkey_load_data = obs.utils.settings.get_arr(unique_id); if hotkey_load_data and hotkey_load_data.data ~= nil then obslua.obs_hotkey_load(hotkey_id, hotkey_load_data.data) hotkey_load_data.free() end obs.register.hotkey_id_list[unique_id] = { id = hotkey_id, title = title, callback = callback, remove = function(rss) if rss == nil then rss = false end -- obs.utils.settings.del(unique_id) if rss then if obs.register.hotkey_id_list[unique_id] and type(obs.register.hotkey_id_list[unique_id].callback) == "function" then obslua.obs_hotkey_unregister( obs.register.hotkey_id_list[unique_id].callback ) end end obs.register.hotkey_id_list[unique_id] = nil end } return obs.register.hotkey_id_list[unique_id] end function obs.register.get_hotkey(unique_id) unique_id = tostring(script_path()) .. "_" .. tostring(unique_id) if obs.register.hotkey_id_list[unique_id] then return obs.register.hotkey_id_list[unique_id] end return nil end function obs.register.event(unique_id, callback) if not callback and unique_id and type(unique_id) == "function" then callback = unique_id unique_id = tostring(script_path()) .. "_" .. obs.utils.get_unique_id(3) .. "_event" else unique_id = tostring(script_path()) .. "_" .. tostring(unique_id) .. "_event" end if type(callback) ~= "function" then obslua.script_log(obslua.LOG_ERROR, "[OBS REGISTER EVENT] Invalid callback provided") return nil end local event_id = obslua.obs_frontend_add_event_callback(callback) obs.register.event_id_list[unique_id] = { id = event_id, callback = callback, unique_id = unique_id, remove = function(rss) if rss == nil then rss = false end if rss then obslua.obs_frontend_remove_event_callback(callback) end obs.register.event_id_list[unique_id] = nil end }; end function obs.register.get_event(unique_id) unique_id = tostring(script_path()) .. "_" .. tostring(unique_id) .. "_event" if obs.register.event_id_list[unique_id] then return obs.register.event_id_list[unique_id] end return nil end -- [[ OBS REGISTER CUSTOM API END]] -- [[ OBS FILTER CUSTOM API]] function obs.script.filter(filter) local self; self = { id = filter and filter.id or obs.utils.get_unique_id(3), type = filter and filter.type or obslua.OBS_SOURCE_TYPE_FILTER, output_flags = filter and filter.output_flags or bit.bor(obslua.OBS_SOURCE_VIDEO), get_height = function(src) return src and src.height or 0 end, get_width = function(src) return src and src.width or 0 end, update = function(_, settings) if not _ or not _.isAlive or (obs.utils and obs.utils.script_shutdown) then return end if filter and type(filter) == "table" and filter["update"] and type(filter["update"]) == "function" then return filter.update(_, obs.PairStack(settings, nil, nil, true)) end end, create = function(settings, source) if filter and type(filter) == "table" and filter["create"] and type(filter["create"]) == "function" then local src = filter.create(obs.PairStack(settings, nil, nil, true)) if src ~= nil and type(src) == "table" then self.src = src src.filter = source src.is_custom = true src.isAlive = true src.settings = obs.PairStack(settings, nil, nil, true) if filter["setup"] and type(filter["setup"]) == "function" then filter.setup(src) end return src end end local src = { filter = source, source = nil, params = nil, height = 0, width = 0, isAlive = true, settings = obs.PairStack(settings, nil, nil, true), } if source ~= nil then local target = obslua.obs_filter_get_parent(source) if target ~= nil then src.source = target src.width = obslua.obs_source_get_base_width(target) src.height = obslua.obs_source_get_base_height(target) end end shader = [[ uniform float4x4 ViewProj; uniform texture2d image; uniform int width; uniform int height; sampler_state textureSampler { Filter = Linear; AddressU = Border; AddressV = Border; BorderColor = 00000000; }; struct VertData { float4 pos : POSITION; float2 uv : TEXCOORD0; }; float4 ps_get(VertData v_in) : TARGET { return image.Sample(textureSampler, v_in.uv.xy); } VertData VSDefault(VertData v_in) { VertData vert_out; vert_out.pos = mul(float4(v_in.pos.xyz, 1.0), ViewProj); vert_out.uv = v_in.uv; return vert_out; } technique Draw { pass { vertex_shader = VSDefault(v_in); pixel_shader = ps_get(v_in); } } ]] obslua.obs_enter_graphics() src.shader = obslua.gs_effect_create(shader, nil, nil) obslua.obs_leave_graphics() if src.shader ~= nil then src.params = { width = obslua.gs_effect_get_param_by_name(src.shader, "width"), height = obslua.gs_effect_get_param_by_name(src.shader, "height"), image = obslua.gs_effect_get_param_by_name(src.shader, "image"), } else return self.destroy() end if filter and filter["setup"] and type(filter["setup"]) == "function" then filter.setup(src, src.settings) end self.src = src obs.time.schedule(function() -- print("[OBS CUSTOM API FILTER] Running post-creation setup...") -- Debug log obs.time.schedule(function() if not src or not src.isAlive or (obs.utils and obs.utils.script_shutdown) then return end if src.filter then src.source = obslua.obs_filter_get_parent(src.filter) if filter and filter["finally"] and type(filter["finally"]) == "function" then -- print("[OBS CUSTOM API FILTER] Running filter.finally...") -- Debug log filter.finally(src) end end src.isInitialized = true end).after(obs.utils.first_time_load and 500 or 1) end).after(function() return obs.utils.loaded end) return src end, destroy = function(src) if not src then return end src.isAlive = false if src and type(src) == "table" and src.shader then obslua.obs_enter_graphics() obslua.gs_effect_destroy(src.shader) obslua.obs_leave_graphics() end if filter and type(filter) == "table" and filter["destroy"] and type(filter["destroy"]) == "function" then filter.destroy(src) end src.source = nil src.filter = nil src.params = nil end, video_tick = function(src, fps) if not src or not src.isAlive or (obs.utils and obs.utils.script_shutdown) then return end if src.source == nil and src.filter then src.source = obslua.obs_filter_get_parent(src.filter) end if src.source and src.filter then src.width = obslua.obs_source_get_base_width(src.source) src.height = obslua.obs_source_get_base_height(src.source) else src.width = 0; src.height = 0 end local __tick = (filter["video_tick"] or filter["tick"]) or function() end __tick(src, fps) end, video_render = function(src) if not src or not src.isAlive or (obs.utils and obs.utils.script_shutdown) then return end if filter and type(filter) == "table" and filter["video_render"] and type(filter["video_render"]) == "function" then local result = filter.video_render(src) if src.is_custom then return result end end if src.source == nil and src.filter then src.source = obslua.obs_filter_get_parent(src.filter) end if src.source and src.filter then src.width = obslua.obs_source_get_base_width(src.source) src.height = obslua.obs_source_get_base_height(src.source) end if src.filter then local width = src.width; local height = src.height if not width or not height or (width <= 0 or height <= 0) then obslua.obs_source_skip_video_filter(src.filter) return nil end if not obslua.obs_source_process_filter_begin( src.filter, obslua.GS_RGBA, obslua.OBS_NO_DIRECT_RENDERING ) then obslua.obs_source_skip_video_filter(src.filter) return nil end if not src.params then obslua.obs_source_process_filter_end(src.filter, src.shader, width, height) return nil end if type(width) == "number" then obslua.gs_effect_set_int(src.params.width, width) end if type(height) == "number" then obslua.gs_effect_set_int(src.params.height, height) end obslua.gs_blend_state_push() obslua.gs_blend_function( obslua.GS_BLEND_ONE, obslua.GS_BLEND_INVSRCALPHA ) if width and height then obslua.obs_source_process_filter_end(src.filter, src.shader, width, height) end obslua.gs_blend_state_pop() end return true end, get_name = function() return filter and filter.name or "Custom Filter" end, get_defaults = function(settings) local defaults = nil if filter and type(filter) == "table" then if filter["get_defaults"] and type(filter["get_defaults"]) == "function" then defaults = filter.get_defaults elseif filter["defaults"] and type(filter["defaults"]) == "function" then defaults = filter.defaults end end if defaults and type(defaults) == "function" then return defaults(obs.PairStack(settings, nil, nil, true)) end end, get_properties = function(src) local properties = nil if filter and type(filter) == "table" then if filter["get_properties"] and type(filter["get_properties"]) == "function" then properties = filter.get_properties elseif filter["properties"] and type(filter["properties"]) == "function" then properties = filter.properties end end if properties and type(properties) == "function" then return properties(src) end return nil end } table.insert(obs.utils.filters, self) if not filter or type(filter) ~= "table" then filter = {} end filter.get_name = self.get_name if not filter.id then filter.id = self.id end filter.get_width = self.get_width filter.get_height = self.get_height filter.type = self.type filter.output_flags = self.output_flags return filter end -- [[ OBS FILTER CUSTOM API END]] -- [[ OBS SCENE API CUSTOM ]] function obs.scene:get_scene(scene_name) local scene; local source_scene; if not scene_name or not type(scene_name) == "string" then source_scene = obslua.obs_frontend_get_current_scene() if not source_scene then return nil end scene = obslua.obs_scene_from_source(source_scene) else source_scene = obslua.obs_get_source_by_name(scene_name) if not source_scene then return nil end scene = obslua.obs_scene_from_source(source_scene) end local obj_scene_t; obj_scene_t = { group_names = function() local scene_items_list = obs.wrap({ data = obslua.obs_scene_enum_items(scene), type = obs.utils.OBS_SCENEITEM_LIST_TYPE }) if scene_items_list == nil or scene_items_list.data == nil then return nil end local list = {} for _, item in ipairs(scene_items_list.data) do local source = obslua.obs_sceneitem_get_source(item) if source ~= nil then local sourceName = obslua.obs_source_get_name(source) if obslua.obs_sceneitem_is_group(item) then table.insert(list, sourceName) end end end scene_items_list.free() return list end, source_names = function(source_id_type) local scene_nodes_name_list = {} local scene_items_list = obs.wrap({ data = obslua.obs_scene_enum_items(scene), type = obs.utils.OBS_SCENEITEM_LIST_TYPE }) for _, item in ipairs(scene_items_list.data) do local source = obslua.obs_sceneitem_get_source(item) if source ~= nil then local sourceName = obslua.obs_source_get_name(source) if source_id_type == nil or type(source_id_type) ~= "string" or source_id_type == "" then table.insert(scene_nodes_name_list, sourceName) else local sourceId = obslua.obs_source_get_id(source) if sourceId == source_id_type then table.insert(scene_nodes_name_list, sourceName) end end source = nil end end scene_items_list.free() return scene_nodes_name_list end, get = function(source_name) if not scene then return nil end local c = 1 local scene_item; local scene_items_list = obs.wrap({ data = obslua.obs_scene_enum_items(scene), type = obs.utils.OBS_SCENEITEM_LIST_TYPE }) if scene_items_list == nil or scene_items_list.data == nil then return nil end for _, item in ipairs(scene_items_list.data) do c = c + 1 local src = obslua.obs_sceneitem_get_source(item) local src_name = obslua.obs_source_get_name(src) if src ~= nil and src_name == source_name then obslua.obs_sceneitem_addref(item) scene_item = obs.wrap({ data = item, type = obs.utils.OBS_SCENEITEM_TYPE, name = source_name }) break end end scene_items_list.free() if scene_item == nil or scene_item.data == nil then return nil end local obj_source_t; obj_source_t = { free = scene_item.free, item = scene_item.data, data = scene_item.data, _busy = false, _queue = {}, _timer = nil, _frame_time = 0.016, _last_run = os.clock(), _cached_info = obslua.obs_transform_info(), _cached_crop = obslua.obs_sceneitem_crop(), _cached_pos = obslua.vec2(), _cached_scale = obslua.vec2(), _virtual = { initialized = false, pos = { x = 0, y = 0 }, scale = { x = 1, y = 1 }, rot = 0, bounds = { x = 0, y = 0 }, alignment = 0, bounds_type = 0 }, _sync_shadow = function() -- if obj_source_t._virtual.initialized then return end obslua.obs_sceneitem_get_info2(obj_source_t.data, obj_source_t._cached_info) obj_source_t._virtual.pos = { x = obj_source_t._cached_info.pos.x, y = obj_source_t._cached_info.pos.y } obj_source_t._virtual.scale = { x = obj_source_t._cached_info.scale.x, y = obj_source_t._cached_info.scale.y } obj_source_t._virtual.rot = obj_source_t._cached_info.rot obj_source_t._virtual.bounds = { x = obj_source_t._cached_info.bounds.x, y = obj_source_t._cached_info.bounds.y } obj_source_t._virtual.alignment = obj_source_t._cached_info.alignment obj_source_t._virtual.bounds_type = obj_source_t._cached_info.bounds_type obj_source_t._virtual.initialized = true -- Initialize cached structs obslua.obs_sceneitem_get_info2(obj_source_t.data, obj_source_t._cached_info) obslua.obs_sceneitem_get_crop(obj_source_t.data, obj_source_t._cached_crop) obslua.obs_sceneitem_get_pos(obj_source_t.data, obj_source_t._cached_pos) obslua.obs_sceneitem_get_scale(obj_source_t.data, obj_source_t._cached_scale) end, _safe_run = function(func) return pcall(function() return func() end) end, rename= function(new_name) if type(new_name) ~= "string" then return false end return obj_source_t._safe_run(function() obslua.obs_source_set_name(obj_source_t.get_source(), new_name) obj_source_t.name = new_name return true end) end, pos = function(val) obj_source_t._sync_shadow() if val == nil or not (type(val) == "table") or (val.x == nil and val.y == nil) then return { x = obj_source_t._virtual.pos.x, y = obj_source_t._virtual.pos.y } end return obj_source_t._safe_run(function() return obj_source_t.transform({ pos = val }) end) end, scale = function(val) obj_source_t._sync_shadow() if val == nil or not (type(val) == "table") then return { x = obj_source_t._virtual.scale.x, y = obj_source_t._virtual.scale.y } end return obj_source_t._safe_run(function() return obj_source_t.transform({ scale = val }) end) end, rot = function(val) obj_source_t._sync_shadow() if val == nil then return obj_source_t._virtual.rot end return obj_source_t._safe_run(function() return obj_source_t.transform({ rot = val }) end) end, align = function(val) obj_source_t._sync_shadow() if val == nil then return obj_source_t._virtual.alignment end return obj_source_t._safe_run(function() return obj_source_t.transform({ alignment = val }) end) end, bounds = function(size) obj_source_t._sync_shadow() if size == nil or not (type(size) == "table") then return { x = obj_source_t._virtual.bounds.x, y = obj_source_t._virtual.bounds.y } end return obj_source_t._safe_run(function() return obj_source_t.transform({ bounds = size }) end) end, width = function(val) obj_source_t._sync_shadow() if val == nil or type(val) ~= "number" then local is_bounded = obj_source_t._virtual.bounds_type ~= obslua.OBS_BOUNDS_NONE local base_w = obslua.obs_source_get_base_width(obj_source_t.get_source()) return is_bounded and obj_source_t._virtual.bounds.x or (base_w * obj_source_t._virtual.scale.x) end return obj_source_t._safe_run(function() return obj_source_t.size({ width=val }) end) end, height = function(val) obj_source_t._sync_shadow() if val == nil or type(val) ~= "number" then local is_bounded = obj_source_t._virtual.bounds_type ~= obslua.OBS_BOUNDS_NONE local base_h = obslua.obs_source_get_base_height(obj_source_t.get_source()) return is_bounded and obj_source_t._virtual.bounds.y or (base_h * obj_source_t._virtual.scale.y) end return obj_source_t._safe_run(function() return obj_source_t.size({ height=val }) end) end, size= function(size) obj_source_t._sync_shadow() local is_bounded = obj_source_t._virtual.bounds_type ~= obslua.OBS_BOUNDS_NONE if size == nil or not (type(size) == "table") then return { x = is_bounded and obj_source_t._virtual.bounds.x or (obslua.obs_source_get_base_width(obj_source_t.get_source()) * obj_source_t._virtual.scale.x), y = is_bounded and obj_source_t._virtual.bounds.y or (obslua.obs_source_get_base_height(obj_source_t.get_source()) * obj_source_t._virtual.scale.y), width = is_bounded and obj_source_t._virtual.bounds.x or (obslua.obs_source_get_base_width(obj_source_t.get_source()) * obj_source_t._virtual.scale.x), height = is_bounded and obj_source_t._virtual.bounds.y or (obslua.obs_source_get_base_height(obj_source_t.get_source()) * obj_source_t._virtual.scale.y) } end return obj_source_t._safe_run(function() if is_bounded then return obj_source_t.transform({ bounds = { x = (size.x and size.x or size.width), y = (size.y and size.y or size.height) } }) else local base_w = obslua.obs_source_get_base_width(obj_source_t.get_source()) local base_h = obslua.obs_source_get_base_height(obj_source_t.get_source()) if base_w > 0 and base_h > 0 then return obj_source_t.transform({ scale = { x = (size.x and size.x or size.width) / base_w, y = (size.y and size.y or size.height) / base_h } }) end end end) end, crop = function(c) if c == nil then obslua.obs_sceneitem_get_crop(obj_source_t.data, obj_source_t._cached_crop) return obj_source_t._cached_crop end return obj_source_t._safe_run(function() -- Use Cached Crop Struct obslua.obs_sceneitem_get_crop(obj_source_t.data, obj_source_t._cached_crop) if c.top then obj_source_t._cached_crop.top = c.top end if c.bottom then obj_source_t._cached_crop.bottom = c.bottom end if c.left then obj_source_t._cached_crop.left = c.left end if c.right then obj_source_t._cached_crop.right = c.right end obslua.obs_sceneitem_set_crop(obj_source_t.data, obj_source_t._cached_crop) return true end) end, transform = function(tf) if obslua.obs_source_removed(obj_source_t.get_source()) then return nil end obj_source_t._sync_shadow() if not tf or not (type(tf) == "userdata" or type(tf) == "table") then return { pos = { x = obj_source_t._virtual.pos.x, y = obj_source_t._virtual.pos.y }, scale = { x = obj_source_t._virtual.scale.x, y = obj_source_t._virtual.scale.y }, rot = obj_source_t._virtual.rot, bounds = { x = obj_source_t._virtual.bounds.x, y = obj_source_t._virtual.bounds.y }, alignment = obj_source_t._virtual.alignment, bounds_type = obj_source_t._virtual.bounds_type } end return obj_source_t._safe_run(function() local info; if type(tf) == "userdata" then info = tf elseif type(tf) == "table" then for k, v in pairs(tf) do if k == "pos" and type(v) == "table" then if type(v.x) == "number" then obj_source_t._virtual.pos.x = v.x obj_source_t._cached_info.pos.x = v.x end if type(v.y) == "number" then obj_source_t._virtual.pos.y = v.y obj_source_t._cached_info.pos.y = v.y end elseif k == "scale" and type(v) == "table" then if type(v.x) == "number" then obj_source_t._virtual.scale.x = v.x obj_source_t._cached_info.scale.x = v.x end if type(v.y) == "number" then obj_source_t._virtual.scale.y = v.y obj_source_t._cached_info.scale.y = v.y end elseif k == "rot" and type(v) == "number" then obj_source_t._virtual.rot = v obj_source_t._cached_info.rot = v elseif k == "bounds" and type(v) == "table" then if type(v.x) == "number" then obj_source_t._virtual.bounds.x = v.x obj_source_t._cached_info.bounds.x = v.x end if type(v.y) == "number" then obj_source_t._virtual.bounds.y = v.y obj_source_t._cached_info.bounds.y = v.y end elseif k == "alignment" and type(v) == "number" then obj_source_t._virtual.alignment = v obj_source_t._cached_info.alignment = v elseif k == "bounds_type" and type(v) == "number" then obj_source_t._virtual.bounds_type = v obj_source_t._cached_info.bounds_type = v end end info = obj_source_t._cached_info else return end obslua.obs_sceneitem_defer_update_begin(obj_source_t.data) obslua.obs_sceneitem_set_info2(obj_source_t.data, info) obslua.obs_sceneitem_defer_update_end(obj_source_t.data) return true end) end, get_source = function() return obslua.obs_sceneitem_get_source(obj_source_t.data) end, get_name = function() return obslua.obs_source_get_name(obj_source_t.get_source()) end, bounding = function() if not obj_source_t or not obj_source_t.data then return 0 end return obslua.obs_sceneitem_get_bounds_type(obj_source_t.data) end, remove = function() if obj_source_t.data == nil then return true end obslua.obs_sceneitem_remove(obj_source_t.data) obj_source_t.free(); obj_source_t.data = nil; obj_source_t.item = nil return true end, hide = function() return obslua.obs_sceneitem_set_visible(obj_source_t.data, false) end, show = function() return obslua.obs_sceneitem_set_visible(obj_source_t.data, true) end, isHidden = function() return obslua.obs_sceneitem_visible(obj_source_t.data) end, insert = { filter = function(filter_source_or_id, name) local filter_ptr = nil local created_locally = false -- 1. Determine if we are creating new (String ID) or adding existing (Source Object) if type(filter_source_or_id) == "string" then local function insert_filter(id) local source_name= obj_source_t.get_name() if not source_name then return nil end local source = obslua.obs_get_source_by_name(source_name) if source == nil then return end local settings = obslua.obs_data_create() local a_name= name or id local filter = obslua.obs_source_create_private(id, a_name, settings) obslua.obs_source_filter_add(source, filter) obslua.obs_data_release(settings) obslua.obs_source_release(source) return filter end filter_ptr= insert_filter(filter_source_or_id) elseif type(filter_source_or_id) == "table" and filter_source_or_id.data then -- Handle custom wrapper object filter_ptr = filter_source_or_id.data obslua.obs_source_filter_add(src, filter_ptr) elseif type(filter_source_or_id) == "userdata" then -- Handle raw userdata filter_ptr = filter_source_or_id obslua.obs_source_filter_add(src, filter_ptr) end if not filter_ptr then return nil end -- 4. Construct and return the wrapper (consistent with obj_source_t.filter) local filter_wrapper = obs.wrap({ data = filter_ptr, type = obs.utils.OBS_SRC_TYPE }) local self; self = { remove = function() local src=nil;local source_name= obj_source_t.get_name() if source_name then src= obslua.obs_get_source_by_name(source_name) end if src then obslua.obs_source_filter_remove(src, filter_wrapper.data) obslua.obs_source_release(src) end self.free() self = nil return true end, commit = function() if self.settings and self.settings.data then obslua.obs_source_update(filter_wrapper.data, self.settings.data) end return self end, data = filter_wrapper.data, free = function() if self.settings and self.settings.data then self.settings.free() end if filter_wrapper and filter_wrapper.data then filter_wrapper.free(); filter_wrapper = nil end end, settings = obs.PairStack(obslua.obs_source_get_settings(filter_wrapper.data)), id = function() return obslua.obs_source_get_unversioned_id(filter_wrapper.data) end, } return self end }, filter = function(name_or_id) local source = obj_source_t.get_source() if not source then return nil end local found_ptr = obslua.obs_source_get_filter_by_name(source, name_or_id) local filter_wrapper = nil if not found_ptr then local fb = function(parent, filter, param) local id = obslua.obs_source_get_unversioned_id(filter) local id2= obslua.obs_source_get_id(filter) if id == name_or_id or id2 == name_or_id then found_ptr = obslua.obs_source_get_ref(filter) return true end return false end local filter_list = obs.wrap({ data = obslua.obs_source_enum_filters(source), type = obs.utils .OBS_SRC_LIST_TYPE }) for _, filter in ipairs(filter_list.data) do if fb(source, filter, nil) then break end end filter_list.free() end if not found_ptr then return nil end filter_wrapper = obs.wrap({ data = found_ptr, type = obs.utils.OBS_SRC_TYPE }) if not filter_wrapper or not filter_wrapper.data then return nil end local self; self = { remove = function() obslua.obs_source_filter_remove(source, filter_wrapper.data) self.free() self = nil return true end, data = filter_wrapper.data, commit = function() if self.settings and self.settings.data and filter_wrapper and filter_wrapper.data then if obslua.obs_source_removed(filter_wrapper.data) then return self end obslua.obs_source_update(filter_wrapper.data, self.settings.data) end return self end, free = function() if self.settings and self.settings.data then self.settings.free() end if filter_wrapper and filter_wrapper.data then filter_wrapper.free(); filter_wrapper = nil end end, settings = obs.PairStack(obslua.obs_source_get_settings(filter_wrapper.data)), id = function() return obslua.obs_source_get_unversioned_id(filter_wrapper.data) end, } return self end, style = { grad = { enable = function() local src = obs.PairStack(obslua.obs_source_get_settings(obj_source_t.get_source())) if not src or not src.data then src = obs.PairStack() end src.bul("gradient", true) obslua.obs_source_update(obj_source_t.get_source(), src.data) src.free() end, disable = function() local src = obs.PairStack(obslua.obs_source_get_settings(obj_source_t.get_source())) if not src or not src.data then src = obs.PairStack() end src.bul("gradient", false) obslua.obs_source_update(obj_source_t.get_source(), src.data) src.free() end, dir = function(val) local src = obs.PairStack(obslua.obs_source_get_settings(obj_source_t.get_source())) if not src or not src.data then src = obs.PairStack() end if val == nil then local tempv = src.dbl("gradient_dir"); src.free(); return tempv end src.dbl("gradient_dir", val) obslua.obs_source_update(obj_source_t.get_source(), src.data) src.free() end, color = function(r, g, b) local src = obs.PairStack(obslua.obs_source_get_settings(obj_source_t.get_source())) if not src or not src.data then src = obs.PairStack() end if not r or not g or not b then local tempv = src.int("gradient_color"); src.free(); return obs.utils.argb_to_rgb(tempv) end src.int("gradient_color", obs.utils.rgb_to_argb(r, g, b)) obslua.obs_source_update(obj_source_t.get_source(), src.data) src.free() return true end, opacity = function(val) local src = obs.PairStack(obslua.obs_source_get_settings(obj_source_t.get_source())) if not src or not src.data then src = obs.PairStack() end if val == nil then local tempv = src.dbl("gradient_opacity"); src.free(); return tempv end src.dbl("gradient_opacity", val) obslua.obs_source_update(obj_source_t.get_source(), src.data) src.free() end }, bg_opacity = function(val) local src = obs.PairStack(obslua.obs_source_get_settings(obj_source_t.get_source())) if not src or not src.data then src = obs.PairStack() end if val == nil then local tempv = src.dbl("bk_opacity"); src.free(); return tempv end src.dbl("bk_opacity", val) obslua.obs_source_update(obj_source_t.get_source(), src.data) src.free() end, opacity = function(val) local src = obs.PairStack(obslua.obs_source_get_settings(obj_source_t.get_source())) if not src or not src.data then src = obs.PairStack() end if val == nil then local tempv = src.dbl("opacity"); src.free(); return tempv end src.dbl("opacity", val) obslua.obs_source_update(obj_source_t.get_source(), src.data) src.free() end, }, font = { size = function(font_size) local src = obs.PairStack( obslua.obs_source_get_settings(obj_source_t.get_source()) ) if not src or not src.data then src = obs.PairStack() end local font = src.get_obj("font") if not font or not font.data then font = obs.PairStack() --font.str("face","Arial") end if font_size == nil or not type(font_size) == "number" or font_size <= 0 then font_size = font.get_int("size") font.free(); src.free(); return font_size else font.int("size", font_size) end font.free(); obslua.obs_source_update(obj_source_t.get_source(), src.data) src.free() return true end, face = function(face_name) end }, text = function(txt) local src = obs.PairStack( obslua.obs_source_get_settings(obj_source_t.get_source()) ) if not src or not src.data then src = obs.PairStack() end local res = true if txt == nil or txt == "" or type(txt) ~= "string" then res = src.get_str("text") if not res == nil then res = "" end else src.str("text", txt) end obslua.obs_source_update(obj_source_t.get_source(), src.data) src.free() return res end, } function obj_source_t.style.bg_color(r, g, b) local src = obs.PairStack( obslua.obs_source_get_settings(obj_source_t.get_source()) ) if not src or not src.data then src = obs.PairStack() end if not r or not g or not b then local tempv = src.int("bk_color") src.free() return obs.utils.argb_to_rgb(tempv) end src.int("bk_color", obs.utils.rgb_to_argb(r, g, b)) obslua.obs_source_update(obj_source_t.get_source(), src.data) src.free() end function obj_source_t.style.color(r, g, b) local src = obs.PairStack( obslua.obs_source_get_settings(obj_source_t.get_source()) ) if not src or not src.data then src = obs.PairStack() end if not r or not g or not b then local tempv = src.int("color") src.free() return obs.utils.argb_to_rgb(tempv) end local src = obs.PairStack( obslua.obs_source_get_settings(obj_source_t.get_source()) ) if not src or not src.data then src = obs.PairStack() end src.int("color", obs.utils.rgb_to_argb(r, g, b)) obslua.obs_source_update(obj_source_t.get_source(), src.data) src.free() end function obj_source_t.style.get() local src = obs.PairStack( obslua.obs_source_get_settings(obj_source_t.get_source()) ) if not src or not src.data then src = obs.PairStack() end local json = src.json(true) src.free() return json end function obj_source_t.style.set(val) local src = obs.PairStack(val) if not src or not src.data then return nil end obslua.obs_source_update(obj_source_t.get_source(), src.data) src.free() end function obj_source_t.opacity(val) local color_filter= obj_source_t.filter("color_filter_v2") if not color_filter or not color_filter.data then color_filter= obj_source_t.insert.filter("color_filter_v2", "Color Correction") if not color_filter or not color_filter.data then return end end if val == nil or type(val) ~= "number" then local tempv = color_filter.settings.dbl("opacity") color_filter.free() return tempv end color_filter.settings.dbl("opacity", val) return color_filter.commit().free() end return obj_source_t end, add = function(source) if not source then return false end local sceneitem = obslua.obs_scene_add(scene, source) if sceneitem == nil then return nil end obslua.obs_sceneitem_addref(sceneitem) local dt = obs.wrap({ data = sceneitem, type = obs.utils.OBS_SCENEITEM_TYPE }) return dt end, free = function() if not source_scene then return end obslua.obs_source_release(source_scene) scene = nil end, release = function() return obj_scene_t.free() end, get_width = function() return obslua.obs_source_get_base_width(source_scene) end, get_height = function() return obslua.obs_source_get_base_height(source_scene) end, data = scene, item = scene, source = source_scene }; return obj_scene_t end function obs.scene:scene_from(source) if not source or type(source) == 'string' then return nil end local sc = obslua.obs_scene_from_source(source) local ss = obslua.obs_scene_get_source(sc) return obs.scene:get_scene(obslua.obs_source_get_name(ss)) end function obs.scene:name() source_scene = obslua.obs_frontend_get_current_scene() if not source_scene then return nil end local source_name = obslua.obs_source_get_name(source_scene) obslua.obs_source_release(source_scene) return source_name end function obs.scene:add_to_scene(source) if not source then return false end local current_source_scene = obslua.obs_frontend_get_current_scene() if not current_source_scene then return false end local current_scene = obslua.obs_scene_from_source(current_source_scene) if not current_scene then obslua.obs_source_release(current_source_scene) return false end obslua.obs_scene_add(current_scene, source) obslua.obs_source_release(current_source_scene) return true end function obs.scene:names() local scenes = obs.wrap({ data = obslua.obs_frontend_get_scenes(), type = obs.utils.OBS_SRC_LIST_TYPE }) local obj_table_t = {} for _, a_scene in pairs(scenes.data) do if a_scene then local scene_source_name = obslua.obs_source_get_name(a_scene) table.insert(obj_table_t, scene_source_name) end end scenes.free() return obj_table_t end function obs.scene:size() local scene = obs.scene:get_scene() if not scene or not scene.data then return nil end local w = scene:get_width() local h = scene:get_height() scene.free() return { width = w, height = h } end -- [[ OBS SCENE API CUSTOM END ]] -- [[ OBS FRONT API ]] function obs.front.source_names() local list = {} local all_sources = obs.wrap({ data = obslua.obs_enum_sources(), type = obs.utils.OBS_SRC_LIST_TYPE }) for _, source in pairs(all_sources.data) do if source then local source_name = obslua.obs_source_get_name(source) table.insert(list, source_name) end end all_sources.free() return list end function obs.front.source(source) local scene; local source_name; if source and type(source) ~= "string" and type(source) == "userdata" then scene = obs.scene:scene_from(source) source_name = obslua.obs_source_get_name(source) elseif type(source) == "string" then source_name = source local temp = obslua.obs_get_source_by_name(source_name) if not temp then return nil end scene = obs.scene:scene_from(temp) obslua.obs_source_release(temp) end if not scene or not scene.data then return end local sct = scene.get(source_name) scene.free() return sct end -- [[ OBS FRONT API END ]] -- [[ OBS SCRIPT PROPERTIES CUSTOM API]] function obs.script:ui(clb, s) if obs.utils.ui then obslua.script_log(obslua.LOG_ERROR, "[SCRIPT.UI] UI is already created") return false end if type(clb) ~= "function" then obslua.script_log(obslua.LOG_ERROR, "[SCRIPT.UI] Invalid callback provided") return false end obs.utils.ui = function() obs.utils.properties = { list = {}, options = {}, } local p = obs.script.create(s) local self = {}; for key, fnc in pairs(obs.script) do self[key] = function(...) return fnc(p, ...) end end clb(self, p) return p end return true end function obs.script.create(settings) local p = obslua.obs_properties_create() if type(settings) == "userdata" then settings = obs.PairStack(settings, nil, nil, true) end obs.utils.properties[p] = settings return p end function obs.script.options(p, unique_id, desc, enum_type_id, enum_format_id) if not desc or type(desc) ~= "string" then desc = "" end if not unique_id or type(unique_id) ~= "string" or unique_id == "" then unique_id = obs.utils.get_unique_id(20) end if enum_format_id == nil then enum_format_id = obs.enum.options.string; end if enum_type_id == nil then enum_type_id = obs.enum.options.default; end local obj = obslua.obs_properties_add_list(p, unique_id, desc, enum_type_id, enum_format_id); if not obj then obslua.script_log(obslua.LOG_ERROR, "[obsapi_custom.lua] Failed to create options property: " .. tostring(unique_id) .. " description: " .. tostring(desc) .. " enum_type_id: " .. tostring(enum_type_id) .. " enum_format_id: " .. tostring(enum_format_id)) return nil end obs.utils.properties.options[unique_id] = { enum_format_id = enum_format_id, enum_type_id = enum_type_id, type = enum_format_id } obs.utils.properties[unique_id] = obs.utils.obs_api_properties_patch(obj, p) return obs.utils.properties[unique_id] end function obs.script.button(p, unique_id, label, callback) if not label or type(label) ~= "string" then label = "button" end if not unique_id or type(unique_id) ~= "string" or unique_id == "" then unique_id = obs.utils.get_unique_id(20) end if type(callback) ~= "function" then callback = function() end end obs.utils.properties[unique_id] = obs.utils.obs_api_properties_patch( obslua.obs_properties_add_button(p, unique_id, label, function(properties_t, property_t) return callback( property_t, properties_t, obs.utils.properties[properties_t] and obs.utils.properties[properties_t] or obs.utils.settings ) end) , p) return obs.utils.properties[unique_id] end function obs.script.label(p, unique_id, text, enum_type) if not text or type(text) ~= "string" then text = "" end if not unique_id or type(unique_id) == nil or unique_id == "" or type(unique_id) ~= "string" then unique_id = obs.utils.get_unique_id(20) end local default_enum_type = obslua.OBS_TEXT_INFO; if (enum_type == nil) then enum_type = default_enum_type end local obj = obs.utils.obs_api_properties_patch(obslua.obs_properties_add_text(p, unique_id, text, default_enum_type), p) if enum_type == obs.enum.text.error then obj.error(text) elseif enum_type == obs.enum.text.warn then obj.warn(text) end obj.type = enum_type; obs.utils.properties[unique_id] = obj return obj; end function obs.script.group(p, unique_id, desc, enum_type) local pp = obs.script.create() if not desc or type(desc) ~= "string" then desc = "" end if not unique_id or type(unique_id) ~= "string" or unique_id == "" then unique_id = obs.utils.get_unique_id(20) end if enum_type == nil then enum_type = obs.enum.group.normal; end obs.utils.properties[unique_id] = obs.utils.obs_api_properties_patch( obslua.obs_properties_add_group(p, unique_id, desc, enum_type, pp), pp) obs.utils.properties[unique_id].parent = pp obs.utils.properties[unique_id].add = {} for key, fnc in pairs(obs.script) do obs.utils.properties[unique_id].add[key] = function(...) return fnc(obs.utils.properties[unique_id].parent, ...) end end return obs.utils.properties[unique_id] end function obs.script.bool(p, unique_id, desc) if not desc or type(desc) ~= "string" then desc = "" end if not unique_id or type(unique_id) ~= "string" or unique_id == "" then unique_id = obs.utils.get_unique_id(20) end obs.utils.properties[unique_id] = obs.utils.obs_api_properties_patch( obslua.obs_properties_add_bool(p, unique_id, desc), p) return obs.utils.properties[unique_id] end function obs.script.path(p, unique_id, desc, enum_type_id, filter_string, default_path_string) if not unique_id or type(unique_id) ~= "string" or unique_id == "" then unique_id = obs.utils.get_unique_id(20) end if not desc or type(desc) ~= "string" then desc = "" end if enum_type_id == nil or type(enum_type_id) ~= "number" then enum_type_id = obs.enum.path.read end if filter_string == nil or type(filter_string) ~= "string" then filter_string = "" end if default_path_string == nil or type(default_path_string) ~= "string" then default_path_string = "" end obs.utils.properties[unique_id] = obs.utils.obs_api_properties_patch( obslua.obs_properties_add_path(p, unique_id, desc, enum_type_id, filter_string, default_path_string), p) return obs.utils.properties[unique_id] end function obs.script.form(properties, title, unique_id) local pp = obs.script.create(); local __exit_click_callback__ = nil; local __onexit_type__ = 1; local __cancel_click_callback__ = nil; local __oncancel_type__ = 1; if unique_id == nil then unique_id = obs.utils.get_unique_id(20) end local group_form = obs.script.group(properties, unique_id, "", pp, obs.enum.group.normal) local label = obs.script.label(pp, unique_id .. "_label", title, obslua.OBS_TEXT_INFO); obs.script.label(pp, "form_tt", "
", obslua.OBS_TEXT_INFO); local ipp = obs.script.create() local group_inner = obs.script.group(pp, unique_id .. "_inner", "", ipp, obs.enum.group.normal) local exit = obs.script.button(pp, unique_id .. "_exit", "Confirm", function(pp, s, ss) if __exit_click_callback__ and type(__exit_click_callback__) == "function" then __exit_click_callback__(pp, s, obs.PairStack(ss, nil, nil, true)) end if __onexit_type__ == -1 then group_form.free() elseif __onexit_type__ == 1 then group_form.hide() end return true end) local cancel = obs.script.button(pp, unique_id .. "_cancel", "Cancel", function(pp, s, ss) if __cancel_click_callback__ and type(__cancel_click_callback__) == "function" then __cancel_click_callback__(pp, s, obs.PairStack(ss, nil, nil, true)) end if __oncancel_type__ == -1 then group_form.free() elseif __oncancel_type__ == 1 then group_form.hide() end return true end) local obj_t; obj_t = { add = { button = function(...) return obs.script.button(ipp, ...) end, options = function(...) return obs.script.options(ipp, ...) end, label = function(...) return obs.script.label(ipp, ...) end, group = function(...) return obs.script.group(ipp, ...) end, bool = function(...) return obs.script.bool(ipp, ...) end, path = function(...) return obs.script.path(ipp, ...) end, input = function(...) return obs.script.input(ipp, ...) end, number = function(...) return obs.script.number(ipp, ...) end }, get = function(name) return obs.script.get(name) end, free = function() group_form.free() ipp = nil pp = nil return true end, data = ipp, item = ipp, confirm = {}, onconfirm = {}, oncancel = {}, cancel = {} } function obj_t.confirm:click(clb) __exit_click_callback__ = clb return obj_t end; function obj_t.confirm:text(title_value) if not title_value or type(title_value) ~= "string" or title_value == "" then return false end exit.text(title_value) return true end function obj_t.onconfirm:hide() __onexit_type__ = 1 return obj_t end; function obj_t.onconfirm:remove() __onexit_type__ = -1 return obj_t end; function obj_t.onconfirm:idle() __onexit_type__ = 0 return obj_t end function obj_t.cancel:click(clb) __cancel_click_callback__ = clb return obj_t end; function obj_t.cancel:text(txt) if not txt or type(txt) ~= "string" or txt == "" then return false end cancel.text(txt) return true end function obj_t.oncancel:idle() __oncancel_type__ = 0 return obj_t end; function obj_t.oncancel:remove() __oncancel_type__ = -1 return obj_t end; function obj_t.oncancel:hide() __oncancel_type__ = 1 return obj_t end function obj_t.show() return group_form.show(); end; function obj_t.hide() return group_form.hide(); end; function obj_t.remove() return obj_t.free() end obs.utils.properties[unique_id] = obj_t return obj_t end function obs.script.fps(properties_t, unique_id, title) if not title or type(title) ~= "string" then title = "" end if not unique_id or type(unique_id) ~= "string" or unique_id == "" then unique_id = obs.utils.get_unique_id(20) end obs.utils.properties[unique_id] = obs.utils.obs_api_properties_patch( obslua.obs_properties_add_frame_rate(properties_t, unique_id, title), properties_t ) return obs.utils.properties[unique_id] end function obs.script.list(properties_t, unique_id, title, enum_type_id, filter_string, default_path_string) if not filter_string or type(filter_string) ~= "string" then filter_string = "" end if not default_path_string or type(default_path_string) ~= "string" then default_path_string = "" end if not enum_type_id or type(enum_type_id) ~= "number" or ( enum_type_id ~= obs.enum.list.string and enum_type_id ~= obs.enum.list.file and enum_type_id ~= obs.enum.list.url ) then enum_type_id = obs.enum.list.string end if not title or type(title) ~= "string" then title = "" end if not unique_id or type(unique_id) ~= "string" or unique_id == "" then unique_id = obs.utils.get_unique_id(20) end obs.utils.properties[unique_id] = obs.utils.obs_api_properties_patch( obslua.obs_properties_add_editable_list( properties_t, unique_id, title, enum_type_id, filter_string, default_path_string ), properties_t) return obs.utils.properties[unique_id] end function obs.script.input(p, unique_id, title, enum_type_id, callback) if not title or type(title) ~= "string" then title = "" end if not unique_id or type(unique_id) ~= "string" or unique_id == "" then unique_id = obs.utils.get_unique_id(20) end if not enum_type_id == nil or ( enum_type_id ~= obs.enum.text.input and enum_type_id ~= obs.enum.text.textarea and enum_type_id ~= obs.enum.text.password) then enum_type_id = obs.enum.text.input end obs.utils.properties[unique_id] = obs.utils.obs_api_properties_patch( obslua.obs_properties_add_text( p, unique_id, title, enum_type_id ), p) return obs.utils.properties[unique_id] end function obs.script.color(properties_t, unique_id, title) if not title or type(title) ~= "string" then title = "" end if not unique_id or type(unique_id) ~= "string" or unique_id == "" then unique_id = obs.utils.get_unique_id(20) end obs.utils.properties[unique_id] = obs.utils.obs_api_properties_patch( obslua.obs_properties_add_color_alpha( properties_t, unique_id, title ), properties_t) return obs.utils.properties[unique_id] end function obs.script.number(properties_t, min, max, steps, unique_id, title, enum_number_type_id, enum_type_id) if not enum_number_type_id then enum_number_type_id = obs.enum.number.int end if not enum_type_id then enum_type_id = obs.enum.number.input end if not unique_id or type(unique_id) ~= "string" or unique_id == "" then unique_id = obs.utils.get_unique_id(20) end local obj; if enum_type_id == obs.enum.number.slider then if enum_number_type_id == obs.enum.number.float then obj = obs.utils.obs_api_properties_patch(obslua.obs_properties_add_float( properties_t, unique_id, title, min, max, steps )) else obj = obs.utils.obs_api_properties_patch(obslua.obs_properties_add_int_slider( properties_t, unique_id, title, min, max, steps )) end else if enum_number_type_id == obs.enum.number.float then obj = obs.utils.obs_api_properties_patch(obslua.obs_properties_add_float( properties_t, unique_id, title, min, max, steps )) else obj = obs.utils.obs_api_properties_patch(obslua.obs_properties_add_int( properties_t, unique_id, title, min, max, steps )) end end if obj then obj["type"] = enum_number_type_id end obs.utils.properties[unique_id] = obj return obj end function obs.script.get(name) return obs.utils.properties[name] end -- [[ OBS SCRIPT PROPERTIES CUSTOM API END ]] -- [[ API UTILS ]] function obs.utils.rgb_to_argb(r, g, b) r = math.max(0, math.min(255, math.floor(r))) g = math.max(0, math.min(255, math.floor(g))) b = math.max(0, math.min(255, math.floor(b))) -- OBS expects the order to be Blue-Green-Red (BGR) -- Red: bits 0-7 (multiply by 1) -- Green: bits 8-15 (multiply by 2^8) -- Blue: bits 16-23 (multiply by 2^16) return (b * 2 ^ 16) + (g * 2 ^ 8) + r end function obs.utils.argb_to_rgb(val) if type(val) ~= "number" then return nil end local b = math.floor(val / 2 ^ 16) % 256 local g = math.floor(val / 2 ^ 8) % 256 local r = val % 256 return r, g, b end function obs.utils.obs_api_properties_patch(pp, pp_t, cb) -- if pp_t ~= nil and not obs.utils.properties[pp] then -- obs.utils.properties[pp]=pp_t; -- end local pp_unique_name = obslua.obs_property_name(pp) local obs_pp_t = pp; -- extra -- onchange [Event Handler] local __onchange_list = {} local item = nil; local objText; local objInput; local objGlobal; objGlobal = { cb = cb, disable = function() obslua.obs_property_set_disabled(pp, true) return nil end, enable = function() obslua.obs_property_set_disabled(obs_pp_t, false) return nil end, onchange = function(callback) if type(callback) ~= "function" then return false end table.insert(__onchange_list, callback) return true end, hide = function() obslua.obs_property_set_visible(obs_pp_t, false) end, show = function() obslua.obs_property_set_visible(obs_pp_t, true) return nil end, get = function() return obs_pp_t end, hint = function(txt) if txt == nil or type(txt) ~= "string" or txt == "" then return obslua.obs_property_long_description(obs_pp_t) end item = obslua.obs_property_set_long_description(obs_pp_t, txt) return nil end, free = function() obs.utils.properties[pp_unique_name] = nil local pv = obslua.obs_properties_get_parent(pp_t) obslua.obs_properties_remove_by_name(pp_t, pp_unique_name) while pv do obslua.obs_properties_remove_by_name(pv, pp_unique_name) pv = obslua.obs_properties_get_parent(pv) end return true end, remove = function() return objGlobal.free() end, data = pp, item = pp, title = function(txt) if txt == nil or type(txt) ~= "string" then return obslua.obs_property_description(pp) end obslua.obs_property_set_description(pp, txt) return objGlobal end, parent = pp_t }; objText = { error = function(txt) if txt == nil or type(txt) ~= "string" then return obslua.obs_property_description(pp) end obslua.obs_property_text_set_info_type(pp, obslua.OBS_TEXT_INFO_ERROR) obslua.obs_property_set_description(pp, txt) return objText end, text = function(txt) local id_name = obslua.obs_property_name(pp) objText.type = obs.enum.text.default obslua.obs_property_text_set_info_type(pp, objText.type) if txt ~= nil and type(txt) == "string" then obslua.obs_property_set_description(pp, txt) end return objText end, warn = function(txt) local id_name = obslua.obs_property_name(pp) local textarea_id = id_name .. "_obsapi_hotfix_textarea" local input_id = id_name .. "_obsapi_hotfix_input" local property = obs.script.get(id_name) local textarea_property = obs.script.get(textarea_id) local input_property = obs.script.get(input_id) objText.type = obs.enum.text.input if property then property.show() end if input_property then input_property.hide() end if textarea_property then textarea_property.hide() end objText.type = obs.enum.text.warn obslua.obs_property_text_set_info_type(pp, objText.type) if txt ~= nil and type(txt) == "string" then obslua.obs_property_set_description(pp, txt) end return objText end, type = -1 }; objInput = { value = obs.expect(function(txt) local settings = nil; if pp_t and obs.utils.properties[pp_t] then settings = obs.utils.properties[pp_t] else settings = obs.utils.settings end if txt ~= nil and type(txt) == "string" then if settings then settings.str(pp_unique_name, txt) end end if settings then return settings.str(pp_unique_name) end return nil end), type = -1 }; local objOption; objOption = { item = nil, clear = function() objOption.item = obslua.obs_property_list_clear(pp) return objOption end, add = { str = function(title, id) if id == nil or type(id) ~= "string" or id == "" then --id= obs.utils.get_unique_id(20) obslua.script_log(obslua.LOG_INFO, "[obs.script.options.str] id is nil or invalid!") return objOption end objOption.item = obslua.obs_property_list_add_string(pp, title, id) return objOption end, int = function(title, id) if id == nil or type(id) ~= "number" then --id= obs.utils.get_unique_id(20) obslua.script_log(obslua.LOG_INFO, "[obs.script.options.int] id is nil or invalid!") return objOption end objOption.item = obslua.obs_property_list_add_int(pp, title, id) return objOption end, dbl = function(title, id) if id == nil or type(id) ~= "number" then --id= obs.utils.get_unique_id(20) obslua.script_log(obslua.LOG_INFO, "[obs.script.options.dbl] id is nil or invalid!") return objOption end objOption.item = obslua.obs_property_list_add_float(pp, title, id) return objOption end, bul = function(title, id) if id == nil or type(id) ~= "boolean" then id = obs.utils.get_unique_id(20) end objOption.item = obslua.obs_property_list_add_bool(pp, title, id) return objOption end }, cursor = function(index) if index == nil or type(index) ~= "number" or index < 0 then if type(index) == "string" then -- find the index by the id value for i = 0, obslua.obs_property_list_item_count(pp) - 1 do if obslua.obs_property_list_item_string(pp, i) == index then index = i break end end if type(index) ~= "number" then return nil end else index = objOption.item; if type(index) ~= "number" or index < 0 then index = obslua.obs_property_list_item_count(pp) - 1 end end end local info_title; local info_id info_title = obslua.obs_property_list_item_name(pp, index) if obs.utils.properties.options[pp_unique_name] and obs.utils.properties.options[pp_unique_name].enum_format_id == obs.enum.options.string then info_id = obslua.obs_property_list_item_string(pp, index) elseif obs.utils.properties.options[pp_unique_name] and obs.utils.properties.options[pp_unique_name].enum_format_id == obs.enum.options.int then info_id = obslua.obs_property_list_item_int(pp, index) elseif obs.utils.properties.options[pp_unique_name] and obs.utils.properties.options[pp_unique_name].enum_format_id == obs.enum.options.float then info_id = obslua.obs_property_list_item_float(pp, index) elseif obs.utils.properties.options[pp_unique_name] and obs.utils.properties.options[pp_unique_name].enum_format_id == obs.enum.options.bool then info_id = obslua.obs_property_list_item_bool(pp, index) else info_id = nil end local nn_obj = nil; nn_obj = { disable = function() obslua.obs_property_list_item_disable(pp, index, true) return nn_obj end, enable = function() obslua.obs_property_list_item_disable(pp, index, false) return nn_obj end, remove = function() obslua.obs_property_list_item_remove(pp, index) return true end, title = info_title, value = info_id, index = index, ret = function() return objOption end, isDisabled = function() return obslua.obs_property_list_item_disabled(pp, index) end } return nn_obj; end, current = function() local current_selected_option = nil local settings = nil; if pp_t and obs.utils.properties[pp_t] then settings = obs.utils.properties[pp_t] else settings = obs.utils.settings end if obs.utils.properties.options[pp_unique_name] and obs.utils.properties.options[pp_unique_name].enum_format_id == obs.enum.options.string then current_selected_option = settings.str(pp_unique_name) elseif obs.utils.properties.options[pp_unique_name] and obs.utils.properties.options[pp_unique_name].enum_format_id == obs.enum.options.int then current_selected_option = settings.int(pp_unique_name) elseif obs.utils.properties.options[pp_unique_name] and obs.utils.properties.options[pp_unique_name].enum_format_id == obs.enum.options.float then current_selected_option = settings.float(pp_unique_name) elseif obs.utils.properties.options[pp_unique_name] and obs.utils.properties.options[pp_unique_name].enum_format_id == obs.enum.options.bool then current_selected_option = settings.bool(pp_unique_name) end return objOption.cursor(current_selected_option) end }; local fr_rt = false local objButton; objButton = { item = nil, click = function(callback) if type(callback) ~= "function" then obslua.script_log(obslua.LOG_ERROR, "[button.click] invalid callback type " .. type(callback) .. " expected function") return objButton end local tk = os.clock() objButton.item = obslua.obs_property_set_modified_callback(pp, function(properties_t, property_t, obs_data_t) if os.clock() - tk <= 0.01 then return true end return callback(properties_t, property_t, obs.PairStack(obs_data_t, nil, nil, true)) end) return objButton end, text = function(txt) if txt == nil or type(txt) ~= "string" or txt == "" then return obslua.obs_property_description(pp) end obslua.obs_property_set_description(pp, txt) return objButton end, url = function(url) if not url or type(url) ~= "string" or url == "" then obslua.script_log(obslua.LOG_ERROR, "[button.url] invalid url type, expected string, got " .. type(url)) return objButton --obslua.obs_property_button_get_url(pp) end obslua.obs_property_button_set_url(pp, url) return objButton end, type = function(button_type) if button_type == nil or (button_type ~= obs.enum.button.url and button_type ~= obs.enum.button.default) then obslua.script_log(obslua.LOG_ERROR, "[button.type] invalid type, expected obs.enum.button.url | obs.enum.button.default, got " .. type(button_type)) return objButton --obslua.obs_property_button_get_type(pp) end obslua.obs_property_button_set_type(pp, button_type) return objButton end }; -- [[ GROUP ]] local objGroup; objGroup = { }; -- local objBool; objBool = { checked = function(bool_value) local settings = nil; if pp_t and obs.utils.properties[pp_t] then settings = obs.utils.properties[pp_t] else settings = obs.utils.settings end if not settings then obslua.script_log(obslua.LOG_ERROR, "[obs.utils.settings] is not set, please use 'script_load' to set it") return nil end local property_id = obslua.obs_property_name(pp) if bool_value == nil or type(bool_value) ~= "boolean" then return settings.get_bul(property_id) end settings.bul(property_id, bool_value) return objBool end, }; local objColor; objColor = { value = obs.expect(function(r_color, g_color, b_color, alpha_value) local settings = nil; if pp_t and obs.utils.properties[pp_t] then settings = obs.utils.properties[pp_t] else settings = obs.utils.settings end if r_color == nil then return settings.int(pp_unique_name) end if type(r_color) ~= "number" or type(g_color) ~= "number" or type(b_color) ~= "number" then return false end if alpha_value == nil then alpha_value = 1 end local color_value = bit.bor( bit.lshift(alpha_value * 255, 24), bit.lshift(b_color, 16), bit.lshift(g_color, 8), r_color ) --(alpha_value << 24) | (b_color << 16) | (g_color << 8) | r_color settings.int(pp_unique_name, color_value) return color_value end), type = obslua.OBS_PROPERTY_COLOR_ALPHA } local objList; objList = { insert = function(value, selected, hidden) if type(value) ~= "string" then return objList end if type(selected) ~= "boolean" then selected = false end if type(hidden) ~= "boolean" then hidden = false end local settings = nil; if pp_t and obs.utils.properties[pp_t] then settings = obs.utils.properties[pp_t] else settings = obs.utils.settings end local unique_id = obs.utils.get_unique_id(20) local obs_data_t = obs.PairStack() obs_data_t.str("value", value) obs_data_t.bul("selected", selected) obs_data_t.bul("hidden", hidden) obs_data_t.str("uuid", unique_id) local obs_curr_data_t = settings.arr(pp_unique_name) obs_curr_data_t.insert(obs_data_t.data) obs_data_t.free(); obs_curr_data_t.free() return objList end, filter = function() return obslua.obs_property_editable_list_filter(pp) end, default = function() return obslua.obs_property_editable_list_default_path(pp) end, type = function() return obslua.obs_property_editable_list_type(pp) end, }; local objNumber; objNumber = { suffix = function(text) obslua.obs_property_float_set_suffix(pp, text) obslua.obs_property_int_set_suffix(pp, text) return objNumber end, value = function(value) local settings = nil; if pp_t and obs.utils.properties[pp_t] then settings = obs.utils.properties[pp_t] else settings = obs.utils.settings end if objNumber.type == obs.enum.number.int then settings.int(pp_unique_name, value) elseif objNumber.type == obs.enum.number.float then settings.dbl(pp_unique_name, value) else return nil end return value end, type = nil } local property_type = obslua.obs_property_get_type(pp) -- [[ ON-CHANGE EVENT HANDLE FOR ANY KIND OF USER INTERACTIVE INPUT ]] if property_type == obslua.OBS_PROPERTY_COLOR or property_type == obslua.OBS_PROPERTY_COLOR_ALPHA or property_type == obslua.OBS_PROPERTY_BOOL or property_type == obslua.OBS_PROPERTY_LIST or property_type == obslua.OBS_PROPERTY_EDITABLE_LIST or property_type == obslua.OBS_PROPERTY_PATH or (property_type == obslua.OBS_PROPERTY_TEXT and ( obslua.obs_property_text_type(pp) == obs.enum.text.textarea or obslua.obs_property_text_type(pp) == obs.enum.text.input or obslua.obs_property_text_type(pp) == obs.enum.text.password )) then local tk = os.clock() obslua.obs_property_set_modified_callback(obs_pp_t, function(properties_t, property_t, settings) if os.clock() - tk <= 0.01 then return true end settings = obs.PairStack(settings, nil, nil, true) local pp_unique_name = obslua.obs_property_name(property_t) local current_value; property_type = obslua.obs_property_get_type(property_t) if property_type == obslua.OBS_PROPERTY_BOOL then current_value = settings.bul(pp_unique_name) elseif property_type == obslua.OBS_PROPERTY_TEXT or property_type == obslua.OBS_PROPERTY_PATH or property_type == obslua.OBS_PROPERTY_BUTTON then current_value = settings.str(pp_unique_name) elseif property_type == obslua.OBS_PROPERTY_INT or property_type == obslua.OBS_PROPERTY_COLOR_ALPHA or property_type == obslua.OBS_PROPERTY_COLOR then current_value = settings.int(pp_unique_name) elseif property_type == obslua.OBS_PROPERTY_FLOAT then current_value = settings.dbl(pp_unique_name) elseif property_type == obslua.OBS_PROPERTY_LIST then if obs.utils.properties.options[pp_unique_name] and obs.utils.properties.options[pp_unique_name].type == obs.enum.options.string then current_value = settings.str(pp_unique_name) elseif obs.utils.properties.options[pp_unique_name] and obs.utils.properties.options[pp_unique_name].type == obs.enum.options.int then current_value = settings.int(pp_unique_name) elseif obs.utils.properties.options[pp_unique_name] and obs.utils.properties.options[pp_unique_name].type == obs.enum.options.float then current_value = settings.dbl(pp_unique_name) elseif obs.utils.properties.options[pp_unique_name] and obs.utils.properties.options[pp_unique_name].type == obs.enum.options.bool then current_value = settings.bul(pp_unique_name) end elseif property_type == obslua.OBS_PROPERTY_FONT then current_value = settings.obj(pp_unique_name) elseif property_type == obslua.OBS_PROPERTY_EDITABLE_LIST then current_value = settings.arr(pp_unique_name) end local result = nil for _, vclb in pairs(__onchange_list) do local temp = vclb(current_value, obs.script.get(obslua.obs_property_name(property_t)), properties_t, settings) if result == nil then result = temp end end if type(current_value) == "table" then current_value.free() end return result end); end if property_type == obslua.OBS_PROPERTY_GROUP then obs.utils.table.append(objGroup, objGlobal) return objGroup; elseif property_type == obslua.OBS_PROPERTY_EDITABLE_LIST then obs.utils.table.append(objList, objGlobal) return objList elseif property_type == obslua.OBS_PROPERTY_LIST then obs.utils.table.append(objOption, objGlobal) return objOption; elseif property_type == obslua.OBS_PROPERTY_INT or property_type == obslua.OBS_PROPERTY_FLOAT then obs.utils.table.append(objNumber, objGlobal) return objNumber elseif property_type == obslua.OBS_PROPERTY_BUTTON then obs.utils.table.append(objButton, objGlobal) return objButton elseif property_type == obslua.OBS_PROPERTY_COLOR_ALPHA or property_type == obslua.OBS_PROPERTY_COLOR then obs.utils.table.append(objColor, objGlobal) return objColor elseif property_type == obslua.OBS_PROPERTY_TEXT then local obj_enum_type_id = obslua.obs_property_text_type(pp) if obj_enum_type_id == obs.enum.text.textarea or obj_enum_type_id == obs.enum.text.input or obj_enum_type_id == obs.enum.text.password then objInput.type = obj_enum_type_id obs.utils.table.append(objInput, objGlobal) return objInput; else objText.type = obj_enum_type_id obs.utils.table.append(objText, objGlobal) return objText; end elseif property_type == obslua.OBS_PROPERTY_BOOL then obs.utils.table.append(objBool, objGlobal) return objBool; else return objGlobal; end end function obs.utils.get_unique_id(rs, i, mpc, cmpc) if type(rs) ~= "number" then rs = 2 end local chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_" if i == nil then i = true; end if mpc == nil or type(mpc) ~= "string" then mpc = tostring(os.time()); mpc = obs.utils.get_unique_id(rs, false, mpc, true) elseif cmpc == true then chars = mpc end local index = math.random(1, #chars) local c = chars:sub(index, index) if c == nil then c = "" end if rs <= 0 then return c; end local val = obs.utils.get_unique_id(rs - 1, false, mpc, cmpc) if i == true and mpc ~= nil and type(mpc) == "string" and #val > 1 then val = val .. "_" .. mpc end return c .. val end function obs.utils.table.append(tb, vv) for k, v in pairs(vv) do if type(v) == "function" then local old_v = v v = function(...) local retValue = old_v(...) if retValue == nil then return tb; end return retValue; end end if type(k) == "string" then tb[k] = v; else table.insert(tb, k, v) end end end function obs.utils.json(s) local i = 1 local function v() i = s:find("%S", i) -- Find next non-whitespace if not i then return nil end local c = s:sub(i, i); i = i + 1 if c == '{' then local r = {} if s:match("^%s*}", i) then i = s:find("}", i) + 1 return r end repeat local k = v() i = s:find(":", i) + 1 r[k] = v() i = s:find("[%,%}]", i) local x = s:sub(i, i) i = i + 1 until x == '}' return r elseif c == '[' then local r = {} if s:match("^%s*]", i) then i = s:find("]", i) + 1 return r end repeat r[#r + 1] = v() i = s:find("[%,%]]", i) local x = s:sub(i, i) i = i + 1 until x == ']' return r elseif c == '"' then local _, e = i, i repeat _, e = s:find('"', e) until s:sub(e - 1, e - 1) ~= "\\" local res = s:sub(i, e - 1):gsub("\\", "") i = e + 1 return res end local n = s:match("^([%-?%d%.eE]+)()", i - 1) if n then i = i + #n - 1 return tonumber(n) end local l = { t = true, f = false, n = nil } i = i + (c == 'f' and 4 or 3) return l[c] end return v() end -- [[ API UTILS END ]] -- [[ OBS CUSTOM API END ]]