Scripting: Easily get Scene from Source

L&Mproducer

New Member
I've written a script (which works miraculously) to automatically move a Source to the top of the display order whenever its visibility is changed from Hidden to Visible. This allows my streamer to use their Stream Deck to "stack" sources in the order they like in real-time.

In the script_properties() function, I ask the user to select a "target" scene that limits this behaviour to only that scene, which contains only these 'stackable' Sources. I then Downstream Keyer to always have that Scene showing, even though the active Scene is our main one with camera, logo, other digital assets etc. This all works great.

Lua:
function script_properties()
    local props = obs.obs_properties_create()
    local p = obs.obs_properties_add_list(props, "TargetScene", "Source Layerer Scene", obs.OBS_COMBO_TYPE_EDITABLE, obs.OBS_COMBO_FORMAT_STRING)
    local scenes = obs.obs_frontend_get_scenes()

    if scenes ~= nil then
        for _, eachScene in ipairs(scenes) do
            local SceneName = obs.obs_source_get_name(eachScene)
            obs.obs_property_list_add_string(p, SceneName, SceneName)
        end
    end

    obs.source_list_release()
    return props
end

To wire up the event, I connect a handler to the "source_show" signal
Lua:
function script_load(settings)
    -- attach a handler to the source-show signal (the "sourceShow_signal" function)
    local sh = obs.obs_get_signal_handler()
    obs.signal_handler_connect(sh, "source_show", sourceShow_signal)
end

Again, no problems here. However, its inside my "sourceShow_signal" function where my novice-level LUA scripting becomes evident and I am requesting your help. Algorithmically, what I want to do is:
  • Determine whether the shown Source is in our TargetScene or not
  • If it is, set its order to OBS_ORDER_MOVE_TOP within the scene
My issue is that I can't seem to find an easy way to get the Scene that contains the Source and compare it to my TargetScene. I've kludged around this by looping through all scenes until I find one that matches my TargetScene, and then looping through the SceneItems looking for the same source name until I find a match, and I hope you'll join me in cringing at the expense, inelegance and potential problems introduced by this. I'm clearly using way too many lines of code to compensate for my lack of knowledge. I'm getting really confused on the different object types (Sources vs Scenes vs SceneItems within the object model).

This seems like an easy optimization for somebody who understands those object types better than I do. It seems to me like this should be a couple lines of code instead of what I'm doing -- which, to be clear, DOES work, but it keeps me up at night :)

Any help would be appreciated, here's the handler function:

Lua:
function sourceShow_signal(cd)
    -- Which OBS source was just shown
    local ShownSource = obs.calldata_source(cd, "source")

    -- make sure we've got an actual object here
    if ShownSource ~= nil then
        -- first we need to make sure that the OBS source that was shown is in our target scene
        -- Couldn't figure out how to get the scene that contains the OBS source directly so we brute force it and search for it

        -- get all scenes
        local scenes = obs.obs_frontend_get_scenes()

        -- as long as we have a collection of scenes
        if scenes ~= nil then
            -- .. let's loop through all scenes. These scenes are "source" objects not scene objects (yet)
            for _, scenesource in ipairs(scenes) do
                -- get the name of the scene currently being looped through
                local scenename = obs.obs_source_get_name(scenesource)
                -- if it's the same scene name as the one we're targetting
                if scenename==TargetScene then
                    -- convert the scenes "source" object to a scene object
                    local scene = obs.obs_scene_from_source(scenesource)
                    -- get all the scene items (OBS sources) in that scene
                    local sceneitems = obs.obs_scene_enum_items(scene)
                    -- loop through those scene items
                    for i, sceneitem in ipairs(sceneitems) do
                        -- convert the sceneitem to a source object
                        local source = obs.obs_sceneitem_get_source(sceneitem)
                        -- get the name of the source object
                        local sourcename = obs.obs_source_get_name(source)
                        -- lastly, get the name of whatever source was just displayed
                        local ShownSourceName = obs.obs_source_get_name(ShownSource)
                        -- compare the two
                        if sourcename == ShownSourceName then
                            -- pop that source to the top of the order
                            obs.obs_sceneitem_set_order(sceneitem,obs.OBS_ORDER_MOVE_TOP)
                        end
                    end
                end
                obs.sceneitem_list_release(sceneitems)
            end
            obs.source_list_release(scenes)
        end
    end
end
 

upgradeQ

Member
I think you should try working directly with scene items in the target scene. There is a signal handler API for scenes.
Check out "item_visible" in the docs (web archive, as the current docs hosting seems to be down at the moment)

Here is the sample code, it may need additional customisation to suit your needs. Currently it attaches to hardcoded "Scene 2". And if you want to change in realtime, you'll also need to use signal_handler_disconnect to change the scene on the fly.

Lua:
for n, v in pairs(obslua) do _G[n] = v end

local function my_callback(calldata)
  local scene_item = calldata_sceneitem(calldata, "item")
  local visible = calldata_bool(calldata, "visible")
  local scene = obs_sceneitem_get_scene(scene_item)
  local scene_name = obs_source_get_name(obs_scene_get_source(scene))
  local scene_item_name = obs_source_get_name(obs_sceneitem_get_source(scene_item))
  print(("item %s has been changed in scene %s to %s"):format(scene_item_name, scene_name, tostring(visible)))
  if visible then
    obs_sceneitem_set_order(scene_item, OBS_ORDER_MOVE_TOP)
  end
end

local function target_target_scene()
  local sources_scene = obs_frontend_get_scenes()
  for _, i in ipairs(sources_scene) do
    local name = obs_source_get_name(i)
    if name == "Scene 2" then
      local sh = obs_source_get_signal_handler(i)
      signal_handler_connect(sh, "item_visible", my_callback)
    end
  end
  source_list_release(sources_scene)
end

local function on_load(event)
  if event == OBS_FRONTEND_EVENT_FINISHED_LOADING then target_target_scene() end
end

function script_load(settings) obs_frontend_add_event_callback(on_load) end

To help with your code, you should fisrt fix obs.source_list_release() first, as it requires an argument.
Then do an early return "if ShownSource == nil then return end", this will help reduce nesting.
Next, use snake_case for your variables as this is the most readable code style. Combine this with meaningful varible names and you can delete most of the comments you write.
P.S. Please don't write Lua as "LUA" see https://www.lua.org/about.html#:~:text="Lua" (pronounced LOO-,that is, "Lua".
 

L&Mproducer

New Member
I really appreciate your time on this. I can see & appreciate how much more elegant and concise this is. It seems that my only problem now is to replace "Scene 2" with the actual Scene name that I have stored in the obs_property_list (which I set through the plugin settings script_properties and such), which doesn't seem to have loaded those values in time (when I hardcode it, it works, and when I try to print the setting just before the comparison, its empty). Is there a better place in the lifecycle for me to do this binding? Or would I be better off storing the TargetScene somehow else?
 

upgradeQ

Member
You can have several ways to bind the target scene, by timer, by callback, on click, etc.
It really depends. Here I bind after load with full OBS restart required (after scene selection)

Lua:
for n, v in pairs(obslua) do _G[n] = v end

local function my_callback(calldata)
  local scene_item = calldata_sceneitem(calldata, "item")
  local visible = calldata_bool(calldata, "visible")
  local scene = obs_sceneitem_get_scene(scene_item)
  local scene_name = obs_source_get_name(obs_scene_get_source(scene))
  local scene_item_name = obs_source_get_name(obs_sceneitem_get_source(scene_item))
  print(("item %s has been changed in scene %s to %s"):format(scene_item_name, scene_name, tostring(visible)))
  if visible then
    obs_sceneitem_set_order(scene_item, OBS_ORDER_MOVE_TOP)
  end
end

local function target_target_scene()
  local sources_scene = obs_frontend_get_scenes()
  for _, i in ipairs(sources_scene) do
    local name = obs_source_get_name(i)
    if name == g_target_scene_name then
      local sh = obs_source_get_signal_handler(i)
      signal_handler_connect(sh, "item_visible", my_callback)
    end
  end
  source_list_release(sources_scene)
end

local function on_load(event)
  if event == OBS_FRONTEND_EVENT_FINISHED_LOADING then target_target_scene() end
end

function script_load(settings) obs_frontend_add_event_callback(on_load) end

function script_update(settings)
  g_target_scene_name = obs_data_get_string(settings, "TargetScene")
end

function script_properties()
    local props = obs_properties_create()
    local p = obs_properties_add_list(props, "TargetScene", "Source Layerer Scene",
    OBS_COMBO_TYPE_EDITABLE, OBS_COMBO_FORMAT_STRING)
    local scenes = obs_frontend_get_scenes()
    if scenes ~= nil then
        for _, eachScene in ipairs(scenes) do
            local SceneName = obs_source_get_name(eachScene)
            obs_property_list_add_string(p, SceneName, SceneName)
        end
    end
    source_list_release(scenes)
    return props
end
It works because useful code is loaded before OBS_FRONTEND_EVENT_FINISHED_LOADING script_update in particular.
On the order of loading read https://obsproject.com/kb/scripting-guide and do print logging, that helps.
If you want a different approach, see other open source scripts using the signal_handler_disconnect API.
It would be interesting to know more about your workflow and your requirements for this script.
 
Top