Getting Started With OBS Scripting

OBS can be extended with Python and Lua scripts. This set of pages explains the basics to get started and describes how to implement common features.

Scripts management window

Scripts are managed by the user through the Scripts dialog window, displayed via the menu item Tools > Scripts:

Main scripts window

A list of scripts currently added to OBS is displayed on the left hand side (here two scripts distributed with OBS). On the right hand side, if any, the description and editable properties of the script are shown. A script can be added by clicking on + and selecting the related Python or Lua file, removed with -. Use the 🗘 button to Reload Scripts and the Defaults button to reset the values of the editable properties to their default values.

For Python scripts, a decent distribution of Python must be installed by the user, and the Python Install Path must be set in the tab Python Settings. Please refer to the OBS scripting documentation for the supported version of Python (currently Python 3.6). Depending on the Python installation, locating the installation path can be difficult, e.g. on Windows 10 with Python installed from the Microsoft Store, the installation path is located at [UserFolder]\AppData\Local\Programs\Python\Python36.

OBS ecosystem

Scripting is a way to add functionality to OBS but it is not the only one. Having an overview of the context is important before going into the details. Here is a non-exhaustive list of methods one could use to develop new functions that interact with OBS:

  • Plugins are typically implemented in C/C++ and compiled into dynamic libraries that are discovered and loaded at run time by OBS. The main advantages of plugins are obviously the performance and the raw access to all functions and libraries of OBS. A significant drawback is the need to setup an OBS compilation environment and to deal with subsequent OBS versions if re-compilation is necessary. In addition, if a wide distribution is envisaged, compilation and installation for different platforms must be considered.
  • Scripts are written in Lua or Python and are interpreted or compiled on-the-fly at run time. Scripts are mostly platform-independent and are usually not affected by slight changes of OBS data structures. They have access to large portions of the OBS C API. Obvious advantages are the simplified development and maintenance while supporting many features of plugins. The drawback is a lower performance, which is not an issue in most cases.
  • Browser docks and sources (for Windows and macOS) can use a Javascript API to react to events in OBS and retrieve basic information. They can be hosted on a web-server or locally, and support what Chromium supports (including HTML5, WebGL, etc). They are typically used as sources for overlays reacting to events.
  • WebSockets can be used to remotely control OBS via a network interface. Different libraries support the protocol for customized applications (C#, Javascript, Python, Java, ..).
  • Numerous plugins and scripts may provide the wished behavior without development. As an example, StreamFX supports customized shaders with customizable input parameters for filter, source or transition video effects.

Comparison of Python and Lua for scripting in OBS

Lua is by far less popular and less powerful than Python, but it is a bit simpler (on purpose, to reduce complexity and footprint in the embedding executable) and better integrated in OBS (as of v26.1):

Criteria Python Lua
General usage Wide usage for everything, full-featured standard library, etc Sparse usage mainly as embedded scripting extension (e.g. in Wireshark, VLC, RPM, etc), poor standard library (use the OBS API or FFI to fill the gaps)
Additional modules Supported e.g. with pip Supported for pure Lua modules, probably possible but complex for modules with binary
Interpreter Not embedded Fully embedded, based on LuaJIT
Supported libobs modules None Sources using source_info

API documentation

OBS has a huge API of C functions and data structures. Scripting-specific API features are described in the OBS scripting documentation. The rest of the API is documented in the original C flavour only, there is no documentation of the functions and data structures as seen from the scripting environment so far.

These resources can be helpful (feel free to add something):

Getting started

A bare text editor is the only thing you need to get started, but prefer the IDE of your choice for a more comfortable editing. There is no official supporting file for features such as Intellisense so far.

Two tutorials are proposed in this Wiki:

  • Start with the Source Shake tutorial that shows how to animate a source displayed in a scene with a hotkey and customizable properties, in Python and Lua
  • The Halftone Filter tutorial is an example of shader-based video filter in Lua

Other tutorial resources (feel free to add something):

Bindings

There is no strong layer of protection or consistency checking between scripting and binary functions. The C functions of the OBS API are bound to the scripting environment through wrapper functions written in C and compiled into OBS libraries.

Typically, when a Python/Lua function is called in a script, the interpreter calls the related C wrapper function that implements these 3 steps:

  1. Validation of the data given as arguments to the script function and conversion of this data from Python/Lua types to C types
  2. Call to the original C function of the API
  3. Conversion of the data returned by the C function from C types to Python/Lua types

Wrapper functions exist as well for "getters" and "setters", in order to access the members of OBS data structures, again with data conversion between C types and Python/Lua types.

Nearly all of wrapper functions are generated automatically by SWIG during OBS build, based on the API functions definition in C. For a few complex C functions and structures, for which SWIG would not generate proper code automatically, wrapper functions are written manually. This is the case for frontend-related functions, source_info and functions with callback arguments (see the section other differences from the C API).

:warning: Even if most functions are usable as intended (especially the ones specifically re-designed for scripting), a few functions with SWIG-written wrappers cannot be used directly for scripting so far, because SWIG cannot interpret properly the data types of arguments or return values given in the C definition. Typically, with values passed by reference or buffers, C pointers and pointer-pointer types are inherently ambiguous.

Script life-cycle

A script must define some global script functions called by the scripting environment at different stages of the script life-cycle and at different execution phases of OBS. The following steps are performed wherever the related global script functions are implemented.

Global script functions called at OBS startup

When a script was previously added to OBS, at startup:

  1. The content of the script file is read and interpreted in a new script execution context (global script functions are defined during this first execution)
  2. script_defaults(settings) is called to initialize default values in data settings
  3. script_description() is called to retrieve a description string to be displayed in the Scripts window (with Qt-style formatting :bulb:)
  4. script_load(settings) is called for one-time initialization, possibly using values of data settings (typically to setup signal handlers)
  5. script_update(settings) is called a first time for initializations depending on the values of data settings (this function will be called again after any change of value in the data settings, see below)

Please note that:

  • The values of the data settings available in settings during OBS startup reflect the state saved at previous OBS closure (properties changed by the user), and are already set in settings when script_defaults, script_load and script_update are called
  • Because OBS does not store the values of properties that were not changed by the user, i.e. properties still set to their default values, such values are not available in settings when script_defaults is called at OBS startup (and are available as set by script_defaults later in script_load and script_update)
  • During OBS startup, the steps 1 to 5 are performed before loading the scenes and sources in the frontend (before OBS_FRONTEND_EVENT_FINISHED_LOADING is emitted). This is especially important in script_update if sources or scenes are looked up.

Check how OBS saves properties in the user's JSON configuration file (at [UserFolder]\AppData\Roaming\obs-studio\basic\scenes under Windows) to better understand what is going on.

Global script function called every frame

During OBS execution, once a script is initialized, script_tick(seconds) is called every rendered frame (seconds is the time in seconds passed since the previous frame). Consider using a timer instead of a recurrent test in script_tick if possible.

:warning: Be very careful with the code in script_tick, OBS may rapidly become unresponsive if some error or text is logged every frame.

Global script functions called at OBS closure

Two global script functions are called when OBS is closed:

  1. script_save(settings) is called just before saving the data settings persistently
  2. script_unload() is called just before the destruction of the script execution context

Data settings are saved automatically at OBS closure, it is not necessary for the script to call any function to save data settings (and not necessary to implement script_save nor script_unload to trigger OBS to save data set by a script).

Global script functions for editable properties

Once OBS startup is completed, script_properties() is called by OBS if the script is selected in the Scripts window (selecting another script would always call the related script_properties function).

The function has to return an obs_properties_t object created using obs_properties_create and filled with GUI elements though obs_properties_add_* functions. The object will be released by OBS when necessary.

A callback function can be set to each property using obs_property_set_modified_callback.

When the value of a property is changed, the sequence is:

  1. script_update(settings) is called (for initializations depending on the values of data settings)
  2. The callback of the modified property is called

Please note that:

  • :bulb: The description of a property supports Qt-style formatting
  • :bulb: A callback function set on a property with obs_property_set_modified_callback has to return true to trigger the refresh of the properties widget (no new call to script_properties)
  • If another property or data setting is modified per script, e.g. in script_update, then its callback is not triggered automatically, use obs_properties_apply_settings to trigger all callbacks (e.g. in script_update with a properties object saved in a global variable in script_properties)

Global script functions call sequence for other operations

The management operations available on the Scripts window trigger more complex sequences (described in this section as in OBS v26.1, the behavior may change in the future).

Adding a script:

  • Initialization steps like in OBS startup except that values of data settings are not available:

    1. First script execution
    2. script_defaults
    3. script_description
    4. script_load
    5. script_update
  • Then, as the script is selected in the Scripts window, the properties are initialized and displayed:

    1. script_properties
    2. Call to all "modified callbacks", for all properties (with data settings still not available)
    3. script_properties again
    4. script_update with data settings available
    5. Call to the "modified callbacks" of properties actually changed in previous steps

:warning: The complete sequence may lead to inconsistencies if not carefully handled, because the same functions are called multiple times, including property callbacks at step 7, while data settings are not available (credits to eukraticism for pointing out this behavior).

Removing a script just triggers a call to script_unload (not script_save).

Resetting to defaults starts with a call to script_update followed by the same steps as for removing and then re-adding a script (data settings not available).

Reloading a script is the same as removing and then re-adding a script, except that the values of data settings are available during the complete sequence.

Troubleshooting

It is common to experience OBS crashes or unexpected behavior during the development. A few hints (feel free to add your own!):

  • Scripts are loaded very early when OBS starts, before the GUI appears. Depending on what the script does at start, it may lead to a very long start time (e.g. shader compilation with unrolled loops).
  • If OBS crashes at start due to a script, or wrong parameters given to a script, it won't be possible to de-activate the faulty script in the Scripts dialog window. To recover: rename the script, start OBS, remove the faulty script from the list of attached scripts, etc.
  • OBS will tend to keep any defined/changed property in the user's JSON configuration file, even if the related property does not exist any more in a later version of the script. Removing a script and then re-adding it (and set again the related properties) will clean-up such ghost properties.
  • OBS may freeze when too much data is logged at once, or may become unresponsive after a couple of minutes. This is especially true when data is logged at every frame, e.g. when a recurrent error occurs in script_tick. It may be necessary to close the OBS window or even kill the process to recover.
  • Many functions in OBS allocate data that needs to be released. This is the meaning of the warning in the documentation: release/destroy objects you reference or create via the API. For instance, a call to obs_get_current_scene must be followed by a call to obs_source_release otherwise OBS may crash on exit (with "Freeing OBS context data" as last log entry). The need to release objects is not always documented. As a rule of thumb, if the API documentation states Returns: A new reference ... or Returns: An incremented reference... then a call to a release function is probably necessary (credits to J. Buchanan for this helpful synthesis).
  • The effects parser misses often unbalanced brackets, without any log (credits to skeletonbow for this general remark). It is recommended to use an IDE able to detect such issues in HLSL (using the extension .hlsl on the effect file name may help).