DEV Community

loading...

Start to write plugins for OBS with Lua

Hector Leiva
Artist who makes interactive work with programming.
Updated on ・4 min read

I often record my D&D sessions with my friends who are all remote via OBS. Open Broadcaster Software (aka OBS) is a: popular, free, and open-source cross-platform streaming and recording program.

I have text that is a timestamp of the day we play D&D on the screen so that whenever we review the footage, we always know from which session it was:

Close up Image with Timestamp

But I didn't want to continue to rewrite the date manually every time.

Sometimes I would forget and the footage would have the incorrect date and it would affect the order of the videos. So I set out to figure out how to write a script in OBS that would set today's date for me.

As a bonus, I wanted to see if I could add a prefix and suffix options to the date as well, just in case I wanted to add additional text before or after the date.


OBS is written in C, C++. But for the purposes of this post is to showcase one of OBS' best features which is that since version 21.0+, anyone can write a script which can use "hooks" that are available in to effectively do anything.

OBS scripting documentation is located here and there are two important notes. You can write your scripts in either Python 3 or Luajit 2 (Lua 5.2) which hook into the C/C++ methods that run OBS.

Despite knowing Python more than Lua; this small task which has a fast iterative cycle seemed like the best time to learn how Lua works.


Figuring out the OBS hooks for writing plugins:

OBS' main hooks are contained in this document but I focused on the following hooks:

script_load
script_update
script_defaults
script_properties

script_load in the "lifecycle" of OBS, is called whenever a new source is created or source is activated. Sources are a collection of inputs (audio/video/moving/static) that map to a specific scene.

script_update is called whenever the user has modified the script that is in use.

script_defaults is called whenever the script is initialized.

script_properties is called to load all the possible options for the script.


Reverse Engineering a Lua Script

I followed a script that comes preloaded with OBS called countdown.lua located within frontend-tools/scripts/ and dissected the following things:

To import the OBS library

obs = obslua

One must set-up global variables

source_name = ""
prefix = ""
suffix = ""

Lua is functional, everything else is a function from here on out. To hook into script_properties, one must make it into a function:

function script_properties()

   ...

end

Specific obs functions must be called to register the source that we are modifying and the options that that would modify the source. Looking at script_properties again, I will showcase the script I wrote for this hook:

obs = obslua

source_name = ""
prefix = ""
suffix = ""

function script_properties()
    local props = obs.obs_properties_create()

    local p = obs.obs_properties_add_list(props, "source", "Text Source", obs.OBS_COMBO_TYPE_EDITABLE, obs.OBS_COMBO_FORMAT_STRING)
    local sources = obs.obs_enum_sources()

    -- As long as the sources are not empty, then
    if sources ~= nil then
        -- iterate over all the sources
        for _, source in ipairs(sources) do
            source_id = obs.obs_source_get_id(source)
            if source_id == "text_gdiplus" or source_id == "text_ft2_source" then
                local name = obs.obs_source_get_name(source)
                obs.obs_property_list_add_string(p, name, name)
            end
        end
    end

    obs.source_list_release(sources)

    obs.obs_properties_add_text(props, "prefix", "Prefix Text", obs.OBS_TEXT_DEFAULT)
    obs.obs_properties_add_text(props, "suffix", "Suffix Text", obs.OBS_TEXT_DEFAULT)

    return props
end
  • obs.obs_properties_create() is used to create an instance of a props data type that maps to an object in C/C++ for OBS.

  • Passing props into obs.obs_properties_add_list(props, "source", "Text Source", obs.OBS_COMBO_TYPE_EDITABLE, obs.OBS_COMBO_FORMAT_STRING) creates a dropdown list that has a user-facing Text Source that maps its values to "source".

The loop that starts here:

local sources = obs.obs_enum_sources()

    -- As long as the sources are not empty, then
    if sources ~= nil then
        -- iterate over all the sources
        for _, source in ipairs(sources) do

is called by loading all the sources that the user has, making sure at least one exists, and then iterating over them. I only want to target sources for which the source is a Text source only. That's what is happening here:

for _, source in ipairs(sources) do
            source_id = obs.obs_source_get_id(source)
            if source_id == "text_gdiplus" or source_id == "text_ft2_source" then
                local name = obs.obs_source_get_name(source)
                obs.obs_property_list_add_string(p, name, name)
            end
        end

If the source has an internal id of "text_gdiplus" or "text_ft2_source", we grab the name of that source and modify the properties of the list from earlier and add the name of that source into the dropdown.

Important note, you are still writing in C/C++

Despite the fact that we can write these scripts in Lua or Python, these scripts are linked to the C/C++ internals of OBS and thus follow the same rules of needing to release memory whenever we no longer need the resource.

That means if we do: local sources = obs.obs_enum_sources(), after we are done with messing with the sources, we need to release this from memory by doing the following:

obs.source_list_release(sources)

Just because we are using higher level languages in this script, that doesn't mean we can forget about memory management.

The last part is needing to add an option for the prefix and suffix and have it available as a free-text field for the user to fill in:

    obs.obs_properties_add_text(props, "prefix", "Prefix Text", obs.OBS_TEXT_DEFAULT)
    obs.obs_properties_add_text(props, "suffix", "Suffix Text", obs.OBS_TEXT_DEFAULT)

Once we are done with setting up the configuration for the script, we need to return the props:

return props

Loaded Script


The other 3 "hooks" for this implementation can be viewed at my Github page where I've made this code available for anyone else to use:

https://github.com/hectorleiva/obs_current_date

Discussion (0)