-- [[ -- Author: @iixisii -- Date: 10.21.25 -- reversion: 3.20.26 -- Version: 5.6.1 -- Description: Hide2Show filter for OBS Studio --]] -- [[ Hide2Show 5.6.1 BLOCK ]] -- [[ variable data ]] os = require("os") bit = require("bit") APP = { SHOW = 1; HIDE = 2; QK = 30; VF = 16; NR = 130;SL=500; STEPS = 10;ACT = { ACT1 = "act1";ACT2 = "act2"; ACT3 = "act3" } } on_exit=false local __settings__ = nil local animation_list = { {id = "lr";name = "From left to right";opt_out = { "act1","act2","act3" },ignore_opt = { "anime_rt_list","anime_rt_cst" }},{id = "rl";name = "From right to left";opt_out = { "act1","act2","act3" },ignore_opt = { "anime_rt_list","anime_rt_cst" }}, {id = "tb";name = "From top to bottom";opt_out = { "act1","act2","act3" },ignore_opt = { "anime_rt_list","anime_rt_cst" }},{id = "bt";name = "From bottom to top";opt_out = { "act1","act2","act3" }, ignore_opt = { "anime_rt_list","anime_rt_cst" }}, } local rotation_list = { {id = "rt_90"; name = "90°"}, {id = "rt_180"; name = "180°"}, {id ="rt_360"; name = "360°"}, {id= "cst", name= "Custom"} } -- [[ Functionality ]] function update_api(src, key, value) local api= src.settings.arr("api") local api_event=api.find("key",key) if not api_event or api_event.data == nil then api_event= obs.PairStack() api.insert(api_event.data) end api_event.str("key",key).bul( "value", value ).bul("is_event", true) api_event.free();api.free() return true end function setup(settings) local Hide2Show= obs.script.filter({ name="Hide2Show| v5.6.1", id="hns-filter-iixisii-vs5.6.0", }); local function init_default(src) if not src or not src.item or not src.item.data then return end src.transform= src.item.transform() src.default= { pos= src.item.pos();scale=src.item.scale(); width=src.item.width();height=src.item.height(); } if src.item.isHidden() then src.defAction= APP.HIDE else src.defAction= APP.SHOW end src.initDefault= false local default = obs.PairStack() default.dbl("pos.x", src.default.pos.x).dbl("pos.y", src.default.pos.y).dbl( "scale.x", src.default.scale.x ).dbl("scale.y", src.default.scale.y).dbl("width", src.default.width).dbl( "height", src.default.height ) src.settings.obj("source_default", default.data) return default.free() end local function init_reset(src) if not src or not src.item or not src.item.data then return end src.item.transform(src.transform) if src.defAction == APP.HIDE then src.item.hide() else src.item.show() end src.initReset= true return true end function Hide2Show.setup(src) src.currTime=os.clock() src.action=APP.HIDE src.init=false src.timer=nil src.anime_target="def" src.anime_rt=nil src.anime_rt_value=nil end function Hide2Show.defaults(settings) settings.bul("action_hide_group",true, true) settings.str("hide_interval_type","mis", true) settings.int("hide_time",1, true) settings.bul("hide_time_random",false, true) -- settings.bul("action_show_group",true, true) settings.str("show_interval_type","mis", true) settings.int("show_time",1, true) settings.bul("show_time_random",false, true) -- settings.int("defAction",-1, true) settings.int("anime_velcity_value", 10, true) settings.str("anime_style_list","def", true) -- api local api= obs.ArrayStack() settings.arr("api", api.data, true) api.free() end function Hide2Show.properties(src) local ui= obs.script.create() local ui_action= obs.script.create() -- [[ Hide ]] local hide_time= obs.script.group(ui, "action_hide_group","Hide Time") local hide_interval_type= hide_time.add.options("hide_interval_type","") hide_interval_type.onchange(function(value) obs.script.get("hide_time").suffix(" " .. tostring(value)) return true end) hide_interval_type.add.str( "milliseconds", "mis" ).add.str("seconds", "sc").add.str( "minutes", "ms" ).add.str("hours", "hr") hide_time.add.number(1, 999999999,1, "hide_time","") hide_time.add.bool("hide_time_random","Randomness") -- [[ Show ]] local show_time= obs.script.group(ui, "action_show_group","Show Time") local show_interval_type= show_time.add.options("show_interval_type","") show_interval_type.onchange(function(value) obs.script.get("show_time").suffix(" " .. tostring(value)) return true end) show_interval_type.add.str( "milliseconds", "mis" ).add.str("seconds", "sc").add.str( "minutes", "ms" ).add.str("hours", "hr") show_time.add.number(1, 999999999,1, "show_time","") show_time.add.bool("show_time_random","Randomness") -- [[ Animation ]] local anime= obs.script.group(ui, "animation_group","Animation") -- [[ Animation List ]] -- [[ Animation Style ]] local anime_style_list= anime.add.options("anime_style_list","Animation: ") anime_style_list.onchange(function(value, p, pp, settings) local speed_list=obs.script.get("anime_speed_list") local velcity_input=obs.script.get("anime_velcity_value") local cooldown= obs.script.get("anime_cooldown") if value ~= "def" then if speed_list then speed_list.show() end if velcity_input then velcity_input.show() end if cooldown then cooldown.show() end else if speed_list then speed_list.hide() end if velcity_input then velcity_input.hide() end if cooldown then cooldown.hide() end end return true end) anime_style_list.add.str( "View animations ( optional )","def" ) for _, iter in pairs(animation_list) do if iter and iter.opt_out ~= nil then local isvalid = true for _, itemValue in pairs(iter.opt_out) do if itemValue == value then isvalid = false break end end if isvalid then anime_style_list.add.str(iter.name, iter.id) if iter.id == current_anime then is_listed = true end end else anime_style_list.add.str(iter.name, iter.id) if iter.id == current_anime then is_listed = true end end end -- [[ Animation Rotation ]] -- [[ Custom Rotation ]] -- [[ Animation Speed ]] local anime_speed_list= anime.add.options("anime_speed_list","Interval: ") anime_speed_list.add.str( "Normal ( system default )", "NR" ).add.str("Quick","QK").add.str( "Very fast","VF" ).add.str("Slow", "SL").hide() -- [[ Animation Velocity ]] local anime_velcity_value= anime.add.number( 1, 1000,1, "anime_velcity_value","Velocity: ", obs.enum.number.int,obs.enum.number.slider ).hide() -- [[ ANIMATION COOLDOWN ]] local anime_cooldown= anime.add.number(0,99999999, 1, "anime_cooldown","Cooldown(ms):").hide() if src.settings.get_str("anime_style_list") and src.settings.get_str("anime_style_list") ~= "def" and src.settings.get_str("anime_style_list") ~= "" then anime_speed_list.show() anime_velcity_value.show() anime_cooldown.show() else anime_speed_list.hide() anime_velcity_value.hide() anime_cooldown.hide() end return ui end function Hide2Show.update(src) if src.action == nil then src.action= APP.HIDE end src.currTime= os.clock() src.time2 = src.settings.get_int("hide_time") src.rnd2 = src.settings.get_bul("hide_time_random") local hide_time_type= src.settings.str("hide_interval_type") if hide_time_type == "sc" then src.time2 = src.time2 * 1000 elseif hide_time_type == "ms" then src.time2= src.time2 * 60000 elseif hide_time_type == "hr" then src.time2= src.time2 * 3600000 end -- src.time1 = src.settings.get_int("show_time") src.rnd1 = src.settings.get_bul("show_time_random") local show_time_type= src.settings.str("show_interval_type") if show_time_type == "sc" then src.time1 = src.time1 * 1000 elseif show_time_type == "ms" then src.time1= src.time1 * 60000 elseif show_time_type == "hr" then src.time1= src.time1 * 3600000 end -- src.cooldown= src.settings.get_int("anime_cooldown") or 0 if type(src.cooldown) ~= "number" or src.cooldown < 0 then src.cooldown= 0 end src.anime_steps = src.settings.get_int("anime_velcity_value") src.anime = src.settings.get_str("anime_style_list") src.anime_time = src.settings.get_str("anime_speed_list") end function Hide2Show.finally(src) local scene_item= obs.front.source(src.source) if scene_item and scene_item.data then src.item= scene_item local defaults= src.settings.get_obj("source_default") if not defaults or not defaults.data or (defaults.dbl("width") == nil or defaults.dbl("width") <= 0) then init_default(src) else src.transform= src.item.transform() if src.item.isHidden() then src.defAction= APP.HIDE else src.defAction= APP.SHOW end src.default= { pos= { x= defaults.dbl("pos.x"), y= defaults.dbl("pos.y"), },scale= { x= defaults.dbl("scale.x"), y= defaults.dbl("scale.y") }, width= defaults.dbl("width"), height= defaults.dbl("height") } end if defaults.data then defaults.free() end Hide2Show.update(src) end end function Hide2Show.video_tick(src) if obs.utils.script_shutdown then return end if not src.isInitialized or not src.item or not src.item.data then return end if not src.filter or not obslua.obs_source_enabled(src.filter) then if not obslua.obs_source_enabled(src.filter) then src.initDefault= true if not src.initReset then init_reset(src) end end src.isLoading= false return end if src.isLoading then return end if src.initDefault then init_default(src) src.initReset= false end local vtime= APP.SL;if src.action == APP.HIDE then if src.rnd2 == true then vtime= math.random(1, src.time2) else vtime= src.time2 end else if src.rnd1 == true then vtime= math.random(1, src.time1) else vtime= src.time1 end end if src.isFromAnimation and(src.cooldown and src.cooldown > 0) then src.isFromAnimation=false src.isLoading=true return obs.time.schedule(function() src.isLoading=false; src.isFromAnimation=false end).after(src.cooldown) end src.isLoading= true;obs.time.schedule(function() if obs.utils.script_shutdown or not src or not src.item or not src.item.data or not src.filter or not obslua.obs_source_enabled(src.filter) then src.isLoading=false return end local function execute() if src.action == APP.HIDE then src.action= APP.SHOW src.isLoading= false return src.item.hide() else src.action= APP.HIDE src.isLoading= false return src.item.show() end end if src.default and src.anime ~="def" then src.isFromAnimation=true src.item.show() -- make the source visible so the user sees the animation. if src.anime == "lr" then local sc = obs.scene:size() obs.time.tick(function(timer, now) if obs.utils.script_shutdown or not src or not src.item or not src.item.data or not src.filter or not obslua.obs_source_enabled(src.filter) then src.isLoading= false if timer and type(timer) == "table" and type(timer.clear) == "function" then timer.clear() end return nil end local pos = src.item.pos() if src.action == APP.SHOW then if pos.x >= src.default.pos.x then if type(timer) == "table" and type(timer.clear) == "function" then timer.clear() end src.item.pos({x = src.default.pos.x}) return execute() else src.item.pos({x = pos.x + src.anime_steps}) end else if pos.x >= sc.width then if type(timer) == "table" and type(timer.clear) == "function" then timer.clear() end src.item.pos({x = -(src.item.width())}) return execute() else src.item.pos({x = pos.x + src.anime_steps}) end end end, APP[src.anime_time] and APP[src.anime_time] or APP.NR) elseif src.anime == "rl" then local sc = obs.scene:size() obs.time.tick(function(timer, now) if obs.utils.script_shutdown or not src or not src.item or not src.item.data or not src.filter or not obslua.obs_source_enabled(src.filter) then src.isLoading= false if timer and type(timer) == "table" and type(timer.clear) == "function" then timer.clear() end return nil end local pos = src.item.pos() if src.action == APP.SHOW then if pos.x <= src.default.pos.x then if type(timer) == "table" and type(timer.clear) == "function" then timer.clear() end src.item.pos({x = src.default.pos.x}) return execute() else src.item.pos({x = pos.x - src.anime_steps}) end else if pos.x <= -(src.item.width()) then if type(timer) == "table" and type(timer.clear) == "function" then timer.clear() end src.item.pos({x = sc.width}) return execute() else src.item.pos({x = pos.x - src.anime_steps}) end end end, APP[src.anime_time] and APP[src.anime_time] or APP.NR) elseif src.anime == "tb" then local sc = obs.scene:size() obs.time.tick(function(timer, now) if obs.utils.script_shutdown or not src or not src.item or not src.item.data or not src.filter or not obslua.obs_source_enabled(src.filter) then src.isLoading= false if timer and type(timer) == "table" and type(timer.clear) == "function" then timer.clear() end return nil end local pos = src.item.pos() if src.action == APP.SHOW then if pos.y >= src.default.pos.y then if type(timer) == "table" and type(timer.clear) == "function" then timer.clear() end src.item.pos({y = src.default.pos.y}) return execute() else src.item.pos({y = pos.y + src.anime_steps}) end else if pos.y >= sc.height then if type(timer) == "table" and type(timer.clear) == "function" then timer.clear() end src.item.pos({y = -(src.item.height())}) return execute() else src.item.pos({y = pos.y + src.anime_steps}) end end end, APP[src.anime_time] and APP[src.anime_time] or APP.NR) elseif src.anime == "bt" then local sc = obs.scene:size() obs.time.tick(function(timer, now) if obs.utils.script_shutdown or not src or not src.item or not src.item.data or not src.filter or not obslua.obs_source_enabled(src.filter) then src.isLoading= false if timer and type(timer) == "table" and type(timer.clear) == "function" then timer.clear() end return nil end local pos = src.item.pos() if src.action == APP.SHOW then if pos.y <= src.default.pos.y then if type(timer) == "table" and type(timer.clear) == "function" then timer.clear() end src.item.pos({y = src.default.pos.y}) return execute() else src.item.pos({y = pos.y - src.anime_steps}) end else if pos.y <= -(src.item.height()) then if type(timer) == "table" and type(timer.clear) == "function" then timer.clear() end src.item.pos({y = sc.height}) return execute() else src.item.pos({y = pos.y - src.anime_steps}) end end end, APP[src.anime_time] and APP[src.anime_time] or APP.NR) else return execute() end else return execute() end end).after(vtime) end function Hide2Show.destroy(src) if src.item and src.item.data then init_reset(src) src.item.free() end end obs.script:ui(function(ui) return ui:label(IndexPage()) end) end function IndexPage() return [[
Select a source and add a filter called 'Hide2Show'.
The time to hide a source in seconds.
The time to show a source in seconds.
When enabled, it will randomize the time it takes to hide/show
You can animate hide/show, by default it is disabled.]] end -- function script_unload() -- for _, filter in pairs(obs.utils.filters) do -- if filter and filter.src and filter.src.timer then -- obslua.timer_remove(filter.src.timer) -- filter.src.timer=nil -- end -- end -- end -- [[ END OF Hide2Show BLOCK ]] --[[ Author: iixisii contact: @iixisii ]] -- [[ 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, enable= function(src, enabled) if filter and type(filter) == "table" and type(filter["enable"]) == "function" then return filter["enable"](src, enabled) end 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, activate= function(src) if filter and type(filter) == "table" and type(filter["activate"]) == "function" then return filter["activate"](src) end end,deactivate= function(src) if filter and type(filter) == "table" and type(filter["deactivate"]) == "function" then return filter["deactivate"](src) end end, hide= function(src) if filter and type(filter) == "table" and type(filter["hide"]) == "function" then return filter["hide"](src) end end,show= function(src) if filter and type(filter) == "table" and type(filter["show"]) == "function" then return filter["show"](src) 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) 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) 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) 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) 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) 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, order= function(obs_enum_order_or_idx) if not obs_enum_order_or_idx or not (type(obs_enum_order_or_idx) == "number") then return obslua.obs_sceneitem_get_order_position(obj_source_t.data) end if obs_enum_order_or_idx == obslua.OBS_ORDER_MOVE_UP or obs_enum_order_or_idx == obslua.OBS_ORDER_MOVE_DOWN or obs_enum_order_or_idx == obslua.OBS_ORDER_MOVE_TOP or obs_enum_order_or_idx == obslua.OBS_ORDER_MOVE_BOTTOM then return obslua.obs_sceneitem_set_order(obj_source_t.data, obs_enum_order_or_idx) else return obslua.obs_sceneitem_set_order_position(obj_source_t.data, obs_enum_order_or_idx) end end, ground=function() return obj_source_t.order(obslua.OBS_ORDER_MOVE_BOTTOM) end,up=function() return obj_source_t.order(obslua.OBS_ORDER_MOVE_UP) end,down=function() return obj_source_t.order(obslua.OBS_ORDER_MOVE_DOWN) end,top=function() return obj_source_t.order(obslua.OBS_ORDER_MOVE_TOP) end, width = function(val) 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()) local crop = obj_source_t._cached_crop -- Calculate effective width after crop local cropped_w = math.max(0, base_w - crop.left - crop.right) return is_bounded and obj_source_t._virtual.bounds.x or (cropped_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) 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()) local crop = obj_source_t._cached_crop -- Calculate effective height after crop local cropped_h = math.max(0, base_h - crop.top - crop.bottom) return is_bounded and obj_source_t._virtual.bounds.y or (cropped_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) 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()) local base_h = obslua.obs_source_get_base_height(obj_source_t.get_source()) local crop = obj_source_t._cached_crop -- Calculate effective dimensions after crop local cropped_w = math.max(0, base_w - crop.left - crop.right) local cropped_h = math.max(0, base_h - crop.top - crop.bottom) if size == nil or not (type(size) == "table") then return { x = is_bounded and obj_source_t._virtual.bounds.x or (cropped_w * obj_source_t._virtual.scale.x), y = is_bounded and obj_source_t._virtual.bounds.y or (cropped_h * obj_source_t._virtual.scale.y), width = is_bounded and obj_source_t._virtual.bounds.x or (cropped_w * obj_source_t._virtual.scale.x), height = is_bounded and obj_source_t._virtual.bounds.y or (cropped_h * 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 if cropped_w > 0 and cropped_h > 0 then return obj_source_t.transform({ scale = { x = (size.x and size.x or size.width) / cropped_w, y = (size.y and size.y or size.height) / cropped_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 pcall(function() return obj_source_t._sync_shadow() 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() local w= obslua.obs_source_get_width(source_scene) return obslua.obs_source_get_base_width(source_scene) end, get_height = function() local h= obslua.obs_source_get_height(source_scene) 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", "