os= require("os")
function setup()
local _= obs.script.filter({
name= "4IM3 (ver| 1.0.3)",id="4im3_filter_v2",
})
function _.update(src)
src.update_tick= os.clock()
local animeList= src.settings.get_str("anime_list")
local durlist= src.settings.get_str("durlist")
local rep= src.settings.get_str("rep")
src.clock= os.clock()
src.seed= 3.5;src.count= tonumber(src.settings.str("custom_input")) or 1
local parsed_tm = get_time(durlist)
src.tm= parsed_tm
src.anime= animeList
src.rep= rep;src.isOnCoolDown= false
src.coolDown= tonumber(src.settings.str("cooldown_input")) or 1000;src.returnValue= false
end
function _.destroy(src)
if src.scene_item and src.scene_item.data then
if src.style then
src.scene_item.style.set(src.style)
end
if src.transform then src.scene_item.transform(src.transform) end
src.scene_item.free()
end
src.transform= nil;src.transform_ok= false;
end
function _.defaults(settings)
settings.str("durlist", "1s", true)
settings.str("rep", "fr", true)
settings.str("anime_list", " Useful Links
")
ui:label([[
4IM3 by iixisii
]] .. about_me())
end)
end
function get_time(value)
if type(value) ~= "string" or value == "" then
return nil
end
-- Trim whitespace
value = value:match("^%s*(.-)%s*$")
if not value or value == "" then
return nil
end
local num, ty = value:match("^(%d+)(%a+)$")
if not num or not ty then
return nil
end
num = tonumber(num)
ty = ty:lower()
local opt= {
ms=1, s= 1000, m= 60000, h= 3600000, d= 86400000
}
if not opt[ty] or num == nil or num <= 0 then
return nil
end
return num * opt[ty]
end
function about_me()
return [[
Video Tutorial: click here
Ver. 1.0.3
Dev
Author: @iixisii
updates follow XDeviixisiiX]] end __animation_list= { floating_v = "Floating (Vertical)", shake = "Shake / Rumble",dvd_bounce = "DVD Screensaver Bounce", ghost_fade= "Battery low", rainbow_cycle="Rainbow step", sway= "Sway",ghost_pulse="Fade pluse",spin="360° Spin", glitch="Glitch Effect", breathing= "Breathing Effect",jello_wobble= "Jello Wobble", orbit= "Orbiting Motion",flash_alert= "Flash Alert", handheld_drift= "Handheld Drift", loading_bar= "Loading Bar", slide_bounce= "Slide In with Bounce" } ui_options = { cooldown_input = { jello_wobble = true,loading_bar= true, slide_bounce= true } } nt= { loading_bar = function(src) if not src.scene_item or not src.scene_item.data then src.isActive=false return false end local bounds= src.scene_item.bounding() local target_percent = nil if bounds == obs.enum.bound.stretch or bounds == obs.enum.bound.scale_inner or bounds == obs.enum.bound.max then target_percent = src.defaultBoundsX / src.defaultWidth elseif bounds ~= 0 and bounds ~= 1 and bounds ~= 4 then src.isActive=false item.free() return false else target_percent= src.defaultScaleX end src.current_fill = src.current_fill or 0 local speed = 0.02 if src.current_fill < target_percent then src.current_fill = src.current_fill + speed else src.current_fill = target_percent end local new_w = src.defaultWidth * src.current_fill -- Check if we are in "Stretch to bounds" mode (Type 1) if bounds == obs.enum.bound.stretch or bounds == obs.enum.bound.scale_inner or bounds == obs.enum.bound.max then src.scene_item.bounds({x = new_w}) else -- Use your implemented width setter for default mode src.scene_item.width(new_w) end src.scene_item.free() if src.current_fill >= target_percent then src.current_fill = nil src.isOnCoolDown= true end src.isActive = false end, handheld_drift = function(src) if not src.scene_item or not src.scene_item.data then return end src.drift_x = (src.drift_x or 0) + 0.02 src.drift_y = (src.drift_y or 0) + 0.03 local dx = math.sin(src.drift_x) * 10 local dy = math.cos(src.drift_y) * 15 src.scene_item.transform({ pos= {x = src.defaultX + dx, y = src.defaultY + dy}, rot=math.sin(src.drift_x * 0.5) * 2 }) src.isActive = false end, flash_alert = function(src) if not src.scene_item or not src.scene_item.data then return end src.flash_progress = (src.flash_progress or 0) + 0.1 -- Fade from Red (255,0,0) back to White (255,255,255) local r = 255 local gb = 255 * math.min(src.flash_progress, 1) src.scene_item.style.grad.enable() src.scene_item.style.grad.color(r, gb, gb) if src.flash_progress >= 1 then src.flash_progress = nil end src.isActive = false end, orbit = function(src) if not src.defaultX or not src.defaultY then src.isActive= false return end if not src.scene_item or not src.scene_item.data then src.isActive = false return end src.orbit_angle = (src.orbit_angle or 0) + 0.05 local radius = src.radius or 50 local newX = src.defaultX + (math.cos(src.orbit_angle) * radius) local newY = src.defaultY + (math.sin(src.orbit_angle) * radius) src.scene_item.pos({x = newX, y = newY}) src.isActive = false end, jello_wobble = function(src, on_done) if not src.scene_item or not src.scene_item.data then return end src.wobble_timer = (src.wobble_timer or 0) + 0.15 -- We run for one full sine cycle (pi * 2) if src.wobble_timer > math.pi * 2 then -- Snap back to exact defaults using your Universal API src.scene_item.size({width= src.defaultWidth, height= src.defaultHeight}) src.isOnCoolDown= true src.wobble_timer = nil src.isActive = false return end -- Calculate the stretch factor (e.g., +20% width, -20% height) local stretch = math.sin(src.wobble_timer) * 0.2 src.scene_item.size({width= src.defaultWidth * (1 + stretch), height= src.defaultHeight * (1 - stretch)}) src.isActive = false end, breathing = function(src) if not src.scene_item or not src.scene_item.data then return end src.breath_timer = (src.breath_timer or 0) + 0.04 local wave = math.sin(src.breath_timer) -- Scale between 1.0 and 1.05 local s = 1 + (wave * 0.025 + 0.025) src.scene_item.scale({x = s, y = s}) -- Pulse background opacity between 30 and 70 local op = 50 + (wave * 20) src.scene_item.style.bg_opacity(math.floor(op)) src.isActive = false end, glitch = function(src) if not src.scene_item or not src.scene_item.data then src.isActive= false return end local intensity = 15 if math.random() > 0.8 then -- Only glitch 20% of frames for "stutter" effect local dx = math.random(-intensity, intensity) local dy = math.random(-intensity, intensity) local ds = 1 + (math.random(-10, 10) / 100) src.scene_item.transform({ scale = {x = ds, y = ds}, pos= {x= src.defaultX + dx, y= src.defaultY + dy} }) else src.scene_item.transform({ scale = {x = 1, y = 1}, pos= {x= src.defaultX, y= src.defaultY} }) end src.isActive = false end, spin = function(src) if not src.scene_item or not src.scene_item.data then return end local speed = src.seed or 5 src.rotation = (src.rotation or 0) + speed if src.rotation >= 360 then src.rotation = 0 end src.scene_item.rot(src.rotation) src.isActive = false end, floating_v = function(src) if not src.scene_item or not src.scene_item.data then return end -- Initialize angle if not present if not src.sine_angle then src.sine_angle = 0 end -- Adjust speed with src.seed (default is 3.5, might be too fast for sine, so we divide) local speed = (src.seed or 3.5) * 0.05 local amplitude = 20 -- How many pixels up/down it moves src.sine_angle = src.sine_angle + speed -- Calculate offset local offset = math.sin(src.sine_angle) * amplitude -- Apply to Default Y src.scene_item.pos({y= src.defaultY + offset}) src.isActive= false end,shake = function(src) if not src.scene_item or not src.scene_item.data then return end -- intensity based on seed local intensity = 10 -- Generate random offset between -intensity and +intensity local dx = math.random(-intensity, intensity) local dy = math.random(-intensity, intensity) src.scene_item.pos({x=src.defaultX + dx, y=src.defaultY + dy}) src.isActive=false end,dvd_bounce = function(src) local size= obs.scene:size() if not size then src.isActive= false return end if not src.scene_item or not src.scene_item.data then src.isActive= false return end local speed = src.seed or 3.5 -- Initialize velocity direction if missing if not src.vel_x then src.vel_x = speed end if not src.vel_y then src.vel_y = speed end local pos= src.scene_item.pos() local cur_x = pos.x local cur_y = pos.y local width = src.defaultWidth or 100 -- You might need to verify src.width is populated correctly local height = src.defaultHeight or 100 -- Calculate next position local next_x = cur_x + src.vel_x local next_y = cur_y + src.vel_y -- Bounce X (Left or Right Edge) if next_x <= 0 then next_x = 0 src.vel_x = math.abs(src.vel_x) -- Force positive elseif next_x >= (size.width - width) then next_x = size.width - width src.vel_x = -math.abs(src.vel_x) -- Force negative end -- Bounce Y (Top or Bottom Edge) if next_y <= 0 then next_y = 0 src.vel_y = math.abs(src.vel_y) elseif next_y >= (size.height - height) then next_y = size.height - height src.vel_y = -math.abs(src.vel_y) end src.scene_item.pos({x =next_x, y= next_y}) src.isActive= false end,ghost_fade = function(src) if not src.scene_item or not src.scene_item.data then src.isActive= false return end if not src.fade_angle then src.fade_angle = 0 end src.fade_angle = src.fade_angle + 0.05 local opacity = 60 + (math.sin(src.fade_angle) * 40) src.scene_item.style.opacity(opacity) src.scene_item.style.bg_opacity(opacity) src.isActive = false end, sway = function(src) if not src.scene_item or not src.scene_item.data then src.isActive= false; return; end src.sway_angle = (src.sway_angle or 0) + 0.05 local tilt = math.sin(src.sway_angle) * 15 -- 15 degree swing src.scene_item.rot(tilt) src.isActive = false end,ghost_pulse = function(src) if not src.scene_item or not src.scene_item.data then src.isActive= false return end src.fade_angle = (src.fade_angle or 0) + 0.08 -- Range: 20 to 100 local op = 60 + (math.sin(src.fade_angle) * 40) src.scene_item.style.opacity(math.floor(op)) src.scene_item.style.bg_opacity(math.floor(op)) src.isActive = false end, rainbow_cycle = function(src) if not src.scene_item or not src.scene_item.data then src.isActive= false return end src.hue = (src.hue or 0) + 0.01 if src.hue > 1 then src.hue = 0 end -- Helper to get RGB from Hue local function h_to_rgb(h) local r, g, b local i = math.floor(h * 6) local f = h * 6 - i local q = 1 - f local t = f i = i % 6 if i == 0 then r,g,b = 1,t,0 elseif i == 1 then r,g,b = q,1,0 elseif i == 2 then r,g,b = 0,1,t elseif i == 3 then r,g,b = 0,q,1 elseif i == 4 then r,g,b = t,0,1 elseif i == 5 then r,g,b = 1,0,q end return r*255, g*255, b*255 end local r, g, b = h_to_rgb(src.hue) src.scene_item.style.grad.enable() src.scene_item.style.grad.color(r, g, b) -- Your API handles the BGR swap inside src.scene_item.style.grad.dir(src.hue * 360) src.isActive = false end, slide_bounce = function(src) if not src.scene_item or not src.scene_item.data then return end src.progress = (src.progress or 0) + 0.04 local t = src.progress -- Slide from 0 width to defaultWidth with a bounce -- Formula: 1 - |cos(t * pi * 1.5)| * e^(-t*3) local bounce_factor = 1 - math.abs(math.cos(t * math.pi * 1.5)) * math.exp(-t * 3) src.scene_item.width(src.defaultWidth * math.min(bounce_factor, 1)) if t >= 1 then src.scene_item.width(src.defaultWidth) src.progress = nil src.isOnCoolDown= true end src.isActive = false end } -- [[ OBS CUSTOM API BEGIN ]] -- [[ OBS CUSTOM CALLBACKS ]] function script_load(settings) obs.utils.script_shutdown = false settings = obs.PairStack(settings, nil, nil, true) obs.utils.settings = settings if setup and type(setup) == "function" then setup(settings) end for _, filter in pairs(obs.utils.filters) do obslua.obs_register_source(filter) end end function script_save(settings) 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 if unset and type(unset) == "function" then return unset() end end function script_defaults(settings) if type(defaults) == "function" then return defaults(obs.PairStack(settings, nil, nil, true)) 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 = {} }, 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") ffi.cdef[[ typedef struct { float x; float y; } vec2; // Define the native C functions we want to access void obs_sceneitem_get_pos(void *item, vec2 *pos); void obs_sceneitem_set_pos(void *item, const vec2 *pos); void obs_sceneitem_get_scale(void *item, vec2 *scale); void obs_sceneitem_set_scale(void *item, const vec2 *scale); void obs_sceneitem_set_rot(void *item, float rot); // We treat the userdata pointers as void* for FFI compatibility ]] 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(timeout) local scheduler_callback = nil local function interval() if interval then obslua.timer_remove(interval) interval = nil else return end -- Safety check if obs.utils.script_shutdown or type(scheduler_callback) ~= "function" then return end return scheduler_callback(scheduler_callback) end local interval_list = {} local self = nil; self = { after = function(callback) if obs.utils.script_shutdown or not interval then return end if type(callback) == "function" or type(timeout) ~= "number" or timeout < 0 then scheduler_callback = callback else obslua.script_log(obslua.LOG_ERROR, "[Scheduler] invalid callback/timeout " .. type(callback)) return false end obslua.timer_add(interval, timeout) table.insert(interval_list, interval) -- Track timer return self end, clear = function() if interval ~= nil then obslua.timer_remove(interval) interval = nil end end, update = function(timeout_t) if type(timeout_t) ~= "number" or timeout_t < 0 then obslua.script_log(obslua.LOG_ERROR, "[Scheduler] invalid timeout value") return false end if type(interval) ~= "function" then obslua.script_log(obslua.LOG_ERROR, "[Scheduler] invalid callback function") return false end obslua.timer_remove(interval) timeout = timeout_t obslua.timer_add(interval, timeout) return self 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 = 0.001 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 -- NOTE: OBS Lua usually returns a table for enum_items, not a C-list. -- If 'sceneitem_list_release' doesn't exist in your API version, remove this line. if obslua.sceneitem_list_release then obslua.sceneitem_list_release(self.data) end elseif self.type == obs.utils.OBS_SRC_LIST_TYPE then if obslua.source_list_release then obslua.source_list_release(self.data) else obslua.obs_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 value == nil 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) return def and obslua.obs_data_get_default_bool(self.data, name) or obslua.obs_data_get_bool(self.data, name) 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 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 self.data = obslua.obs_data_create() end elseif type(stack) == "userdata" then self.data = stack else 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) -- 1. Check custom create logic 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 -- Ensure isAlive is set for custom sources too if filter["setup"] and type(filter["setup"]) == "function" then filter.setup(src) end return src end end -- 2. Default creation local src = { filter = source, source = nil, params = nil, height = 0, width = 0, isAlive = true, -- explicit alive flag settings = obs.PairStack(settings, nil, nil, true), aliveScheduledEvents = {}, } -- 3. Initial sizing (Safe check) 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 -- 4. Asynchronous Source Assignment (SAFER) obs.time.schedule(380).after(function() -- CRITICAL: Check shutdown before touching C-pointers if not src or not src.isAlive or (obs.utils and obs.utils.script_shutdown) then return end -- Verify filter still exists in OBS if src.filter then src.source = obslua.obs_filter_get_parent(src.filter) if filter and filter["finally"] and type(filter["finally"]) == "function" then filter.finally(src) end end src.isInitialized = true end) return src end, destroy = function(src) if not src then return end src.isAlive = false -- Mark dead immediately 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 -- Clear references to prevent dangling pointer crashes src.source = nil src.filter = nil src.params = nil end, video_tick = function(src, fps) -- 1. CRITICAL SAFETY CHECK if not src or not src.isAlive or (obs.utils and obs.utils.script_shutdown) then return end -- 2. Fallback: Try to get parent if missing (Common in startup) if src.source == nil and src.filter then src.source = obslua.obs_filter_get_parent(src.filter) end -- 3. Update Dimensions (Preventing the Crash) -- Only attempt this if we have a valid source pointer 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 -- 4. Execute User Tick local __tick = (filter["video_tick"] or filter["tick"]) or function() end __tick(src, fps) end, video_render = function(src) -- 1. CRITICAL SAFETY CHECK 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 -- 2. Validate Source/Filter before rendering 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 -- 3. Standard Filter Process if src.filter then local width = src.width; local height = src.height 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() 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_set_info2(obj_source_t.data, info) 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", "