Start to write plugins for OBS with Lua
Tags: lua, obs
Published: 2019-10-17 04:32:21

I often record my D&D sessions with my friends who are all remote via  OBS. [Open Broadcaster Software](https://en.wikipedia.org/wiki/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](https://thepracticaldev.s3.amazonaws.com/i/1ocfwj5p9v5lqv4vp7xu.png)

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](https://obsproject.com/docs/scripting.html) 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](https://obsproject.com/docs/scripting.html#script-function-exports) but I focused on the following hooks: 

```lua
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

```lua
obs = obslua
```

One must set-up global variables

```lua
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:

```lua
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:

```lua
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:

```lua
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:

```lua
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:

```lua
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:

```lua
    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`:

```lua
return props
```

![Loaded Script](https://thepracticaldev.s3.amazonaws.com/i/v9c31hr0zrjuubsws837.png)

---

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